【Unity】NavMeshを使って敵キャラを障害物のないところに出現(Instantiate)させる

2024年4月10日

ドラクエのようなRPGで敵オブジェクトを自動的に出現させるための方法について考えましょう
作成にあたっては、次のような条件が必要になると思われます

  1. 障害物がある場所には出現させない(障害物に埋もれるから)
  2. プレイヤーから距離を空ける(すぐやられるので)
  3. 出現場所はランダム
  4. 出現時間は一定時間ごと

仕組み

ベクトルとベクトルの回転、NavMeshを使った判断を組み合わせて実現します

チュートリアルで確認

チュートリアルを通して、検証してみましょう

シーンの作成、オブジェクトの配置

必要に応じてEnemyAppearanceプロジェクトを作成します。

3Dでプロジェクトを作成します。

床を作成します。

3DオブジェクトでPlaneを選択します。
サイズを大きくするために Scale(10, 1, 10)にしておきます。

プレイヤーを作成します。

3DオブジェクトでCupsuleを選択します。
Position(0, 1, 0)に変更して、床の上に移動しておきます

障害物を作成します。

3DオブジェクトでCubeを選択します。
Position(10, 0.5, 0)に変更して10m離れた場所に設置します。
Scale(3, 1, 3)に変更して、サイズを大きくしておきます。

NavMeshの作成

PackageManagerからAIのインストール

必要に応じてWindowメニューのPackageMangerからAIをインストールします

NavMesh Surfaceの作成

Hierarchyビューを右クリック(または+をクリック)して、NavMesh Surfaceゲームオブジェクトを作成します

NavMeshをベイクします

NavMesh Surfaceゲームオブジェクトを選択し、NavMeshSurfaceコンポーネントのBakeをクリックします

ベイクの結果ですが、なんと、障害物の上を歩けちゃいますね。
これでは、困りますので、設定を変更します。

歩いてはいけないエリアの指定

障害物の上は歩けないように設定します。

障害物をクリックして選択します。

NavMesh Modifierコンポーネントをアタッチ

歩けないエリア(Not Walkable)に設定

再度、ベイクします。

敵の作成

敵のプレファブを作成する

球(Sphere)を敵に見立てて、ゲームオブジェクトを作成
ProjectビューにD&Dしてプレファブを作成します。
元の球は消しておきます。

スクリプトの作成

テスト用スクリプトを作成

プレイヤーを中心に半径10mのところに敵を作成するテストをします。
作る場所は、20°ずつ回転された場所になります。

using UnityEngine;
using UnityEngine.AI;

public class Spawner : MonoBehaviour
{
    // プレイヤーの位置
    [SerializeField]
    Transform playerTransform;

    // エネミープレファブ
    [SerializeField]
    GameObject enemyPrefab;

    // 回転させる角度(初期は0。テストでは、コードの中で20°ずつ増やしていく)
    int yAngle = 0;

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            SpawnEnemy();
        }
    }

    private void SpawnEnemy()
    {
        // 長さ10mのベクトルをプレイヤーと敵との間の距離とし、変数に代入
        var basePosition = new Vector3(10, 0);

        // 上記のポジションを、(0, 0, 0)を中心(原点)として、y軸を中心に20°(yAngle分)回転させた位置を取得
        var spawnPositioAfterRotate = Quaternion.Euler(0, yAngle, 0) * basePosition;

        // プレイヤーの位置を足すことで、プレイヤー位置から一定距離(10m)の円を描いた位置を取得
        var enemySpawnPositon = playerTransform.position + spawnPositioAfterRotate;

        // もし、どのようにしても置けない時だけ、結果がfalseになる
        // 第4引数の数値(10)は、指定された位置からどれだけ離れている位置までを検索範囲とするかを指定します。この例では、指定された位置から10メートル以内の範囲を検索します。
        if (NavMesh.SamplePosition(enemySpawnPositon, out NavMeshHit navMeshHit, 10, NavMesh.AllAreas))
        {
            // navMeshHit変数は、
            //   NavMeshベイクエリアに置ける場合は、spawnPositionの情報が代入される
            //   NavMeshベイクエリアじゃない場合、一番近いNavMeshベイクエリアの情報が代入される
            // Instantiate()メソッドは次のような引数も取れます
            //  Instantiate(作成するオブジェクト, 作成する位置, 作成する角度)
            Instantiate(enemyPrefab, navMeshHit.position, Quaternion.identity);
            Debug.Log("敵を置ける");
        }
        else
        {
            Debug.Log("敵は置けない");
        }

        // 角度を20°増やして繰り返す
        yAngle += 20;
    }
}
if (NavMesh.SamplePosition(enemySpawnPositon, out NavMeshHit navMeshHit, 10, NavMesh.AllAreas))
{
}

このコード行は、Unityのナビゲーションメッシュ(NavMesh)システムを使用して、特定の位置に敵を生成するための位置を探しています。NavMesh.SamplePositionメソッドは、指定された位置がナビゲーションメッシュ上に存在するかどうかを確認し、存在する場合はその位置情報を提供します。このメソッドは、敵をナビゲーション可能な地形上にのみ生成させるために使われます。具体的には以下の通りです:

  • 引数の説明
    • enemySpawnPosition:敵を生成しようとする位置(プレイヤーの周りの特定の点)。
    • out NavMeshHit navMeshHit:メソッドがtrueを返した場合、この変数にはナビゲーションメッシュ上の位置情報が格納されます。これには、最も近いナビゲーションメッシュ上の点の情報が含まれ、敵を正確に配置するのに使用できます。
    • 10:この数値は、指定された位置からどれだけ離れている位置までを検索範囲とするかを指定します。この例では、指定された位置から10メートル以内の範囲を検索します。
    • NavMesh.AllAreas:この引数は、検索に含めるナビゲーションメッシュエリアのタイプを指定します。NavMesh.AllAreasは、すべてのナビゲーションエリアを検索範囲に含めることを意味します。
  • 戻り値
    • このメソッドは、指定された位置がナビゲーションメッシュ上に存在する(またはナビゲーションメッシュ上に十分近い位置が見つかる)場合にtrueを返します。そうでなければfalseを返します。
  • 使用例
    • このメソッドがtrueを返すと、navMeshHitにナビゲーションメッシュ上の正確な位置が格納されます。その情報を使用して、ゲーム内で敵キャラクターを実際にインスタンス化(生成)します。このプロセスにより、敵が適切にナビゲートできる地形上にのみ配置されることが保証されます。

NavMeshHitクラスについて

NavMeshHitクラスは、Unityのナビゲーションシステム内でナビゲーションメッシュ(NavMesh)のクエリ結果を格納するために使用されます。Unityのナビゲーションメッシュは、キャラクターが移動可能な地形を定義し、パスファインディングや障害物回避などの機能を提供します。NavMeshHitクラスは、特定の位置がナビゲーションメッシュ上にあるかどうかを調べる際や、特定の点から最も近いナビゲーションメッシュ上の点を見つける際などに、そのクエリの結果情報を保持します。

NavMeshHitクラスには、以下のようなプロパティが含まれています:

  • position: クエリによって見つかったナビゲーションメッシュ上の点の位置(Vector3)。
  • distance: クエリを実行した元の位置から、見つかったナビゲーションメッシュ上の点までの距離。
  • normal: ナビゲーションメッシュ上の点での法線ベクトル。これは、メッシュの表面がどのように向いているかを示します。
  • mask: ヒットしたナビゲーションエリアのタイプを表すビットマスク。
  • hit: クエリがナビゲーションメッシュ上の点を見つけたかどうかを示すブール値。

例えば、NavMesh.SamplePositionメソッドを使用してナビゲーションメッシュ上の位置を検索する際、このメソッドはNavMeshHitのインスタンスを出力パラメータとして使用し、見つかった位置の情報を返します。開発者はこのNavMeshHitインスタンスを通じて、検索結果の詳細情報にアクセスできます。

NavMeshHitクラスは、ナビゲーションメッシュ上での位置探索、エージェントの移動計画、エリアの調査など、ナビゲーションに関連するさまざまな操作で重要な役割を果たします。これにより、開発者はゲーム内でキャラクターがスムーズに移動できるようにし、リアルタイムでのパスファインディングや障害物回避の機能を実現できます。

if (NavMesh.SamplePosition(enemySpawnPositon, out NavMeshHit navMeshHit, 10, NavMesh.AllAreas))
{
    Instantiate(enemyPrefab, navMeshHit.position, Quaternion.identity);
    Debug.Log("敵を置ける");
}

NavMesh(ナビゲーションメッシュ)を使用して敵オブジェクトを生成する方法を示しています。NavMeshは、ゲーム内のキャラクターが移動できるエリアを定義します。このスクリプトでは、特定の位置に敵をスポーン(生成)しようとしていますが、その位置が直接NavMesh上にない場合でも、最も近いNavMesh上の位置に敵を配置します。

まず、NavMesh.SamplePositionメソッドを使用して、指定されたenemySpawnPositionがNavMesh上にあるかどうかを確認します。このメソッドは、指定された位置がNavMesh上にない場合でも、指定された範囲内で最も近いNavMesh上の位置を見つけます。out NavMeshHit navMeshHitパラメーターを通じて、この位置の情報が返されます。

メソッドがtrueを返した場合、つまりNavMesh上に位置を見つけることができた場合、Instantiateメソッドを使用してenemyPrefabを指定されたnavMeshHit.positionにインスタンス化します。ここで、Quaternion.identityを使用してオブジェクトの回転を指定していますが、これはオブジェクトが回転せずにそのままの向きで生成されることを意味します。

スクリプトの最後にはDebug.Log("敵を置ける");があり、これは敵が正常に配置されたことをコンソールにログ出力するためのものです。

このスクリプトを使うことで、ゲーム開発者はプレイヤーがナビゲーション可能な地形上に正確に敵を配置することができます。これは、例えばプレイヤーが敵に遭遇するエリアを精密に設計したい場合や、ランダムに敵を生成したいが、それが移動可能な地域内であることを保証したい場合などに便利です。

空のゲームオブっジェクトを作成し、Spawnerスクリプトをアタッチします。
playerTransformには、Capsuleを
enemyPrefabには、作成したプレファブをアウトレット接続します。

テスト結果

実行してみましょう。
最初の敵は、出現場所が障害物の位置になるので、一番近いところに配置されているのがわかります。
2番目以降は、20°ずつ、プレイヤーから10m離れて出現しているのがわかると思います。

補足

プレイヤーに見立てたCapselオブジェクトをNavMeshのベイクエリアに含めない方法

上記手順では、次のようにプレイヤーがベイクエリアの計算に含まれているため、エージェントの移動範囲から外されます
プレイヤーは移動するため、これでは困りますね
ここでは、指定したレイヤーは、計算の対象外とすることで、ベイクされないようにしてみます

指定したレイヤは、移動範囲の計算に入れないようにします

レイヤを新規で作成します

今回は、MovingObjとしました

プレイヤーのレイヤを作成したMovingObjに変更します

ベイクするエリアからMovingObjを除外します

次のようにMix…となります

再度Bakeボタンをクリックすると、このオブジェクトが計算対象が担っているのがわかります

参考

Instantiateの解説は、ジェネリック版を参照するのが今では一般的です。

Unity,小技

Posted by hidepon