Unityでざっくり人流シミュレーション[NavMeshAgentの利用]その2 ~NavMeshの編集と目的地の割り振り~


はじめに

 本記事では、BIMモデルを利用してUnity上で簡単な人流シミュレーションを行うまでの流れをまとめたシリーズです。
 建物の設計業務を行う際の、部屋の配置や動線計画などのラフな判断材料としての利用、あるいはゲーム開発を行う際の手助けになれば幸いです。
 なお、「簡単な」とあるように、学術的に提唱されている手法や懸案事項については触れていないため、精度云々についてはご指摘いただいても対応しかねます。

 また、この記事はシリーズの2番目の内容となりますので、基本的な内容から確認したい方は前回の記事からご覧ください。

この記事で行うこと

 この記事では以下2点の内容を中心に取り扱います。

  • NavMeshの編集(壁を認識させる)
  • 目的地の動的割り振り

NavMeshの編集(壁を認識させる)

 それでは早速中身に入っていきましょう。
 前回の最後でエージェントを動かすところまで行きましたが、エージェントは壁をすり抜けてまっすぐ目的地へと向かってしまっていました。
 そこでここでは、壁を歩行不可能な領域として設定していきます。

 作業自体は非常に簡単です。壁となるオブジェクトを全て選択し、「Navigation」ウィンドウ>「Object」>「Navigation Static」にチェックを入れ、「Navigation Area」を「Not Walkable」に変更します。

 ただし、このままでは部屋に入ることすらできなくなってしまうので、ドアを作成している人はドアのNavigation Areaを「Walkable」に、ドアを作成していない人はドアが配置されるであろう壁を「Walkable」に設定しておきましょう。

ここまでできたら「Bake」タブで「Bake」ボタンをクリックし、NavMeshを更新してください

ドア部分がうまく歩行可能なエリアになっていない場合
「Bake」タブのエージェントに関する設定でエージェントのサイズを変更してみてください。)

bake作業が完了したら、シーンを実行して動きを確認してみてください。
だいたい人が動く感じでエージェントが移動していればOKです。

目的地の動的割り振り

 次に、これまでは1つの目的地に向かうことしかしていませんでしたが、次はエージェント(この例では児童)の目的地をそれぞれに動的に割り当てていく処理を作成していきます。

エージェントの視認性向上のための工夫

 コード実装の前に、今のままではエージェントが小さいため建物規模が大きくなると見づらくなってしまいます。 そこで、エージェントの回避行動に影響せずにデフォルメするためにエージェントを編集していきます。

 下図のように、作成していたCylinderの子要素としてさらにCylinderを作成し、半透明な色を付けます。
 この子要素のサイズを二回りほど大きくし、エージェントの位置を示す領域を分かりやすくしましょう。

エージェントのPrefab化

 エージェントの編集が完了したら、Prefab化を行います。

エージェントの生成と目的地の割り振り

 ここからは、先ほど作成したPrefabをスクリプトでインスタンス化し、同時に目的地を割り当てていく処理を作成していきます。

 まずは前回作成したスクリプトを一部変更し、パラメータを受け付けるように修正を行っていきます(「新たに追加」と「ここを修正」の部分を変更してください)。

AgentController.cs

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

public class AgentController : MonoBehaviour
{
    public string className; // ← 新たに追加

    NavMeshAgent agent;  // NavMeshAgentコンポーネントを格納
    GameObject destination;  // 目的地のSphereを格納

    void Start()
    {
        // NavMeshAgentコンポーネントと目的地のオブジェクトを取得
        agent = GetComponent<NavMeshAgent>();
        destination = GameObject.Find(className);  // 目的地に設定した部屋名を指定 ← ここを修正

        if (agent.pathStatus != NavMeshPathStatus.PathInvalid)
        {
            // 目的地を指定(目的地のオブジェクトの位置情報を渡す)
            agent.SetDestination(destination.transform.position);
        }
    }

}

 次にC#スクリプトを新たに作成し、任意の名前を付けましょう(例:AgentGenerator.cs など)。

AgentGenerator.cs

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

public class AgentGenerator : MonoBehaviour
{
    public GameObject agent;  // AgentのPrefab

    void Start()
    {
        for (int i = 1; i <= 6; i++)  // 1年生から6年生を想定
        {
            for (int j = 1; j <= 2; j++) // 1学年2クラスを想定
            {
                for (int k = 1; k <= 35; k++)  // 1クラス35名を想定
                {
                    // 各クラスを「1-1」などで表現
                    string belongs = i + "-" + j;
                    agent.GetComponent<AgentController>().className = belongs;  // 所属しているクラスの情報をAgentControllerに渡す
                    Instantiate(agent, new Vector3(27f, 0.3f, -12.8f), new Quaternion(0f, 0f, 0f, 0f));
                }
            }
        }
    }

}

 今回は学校を例に挙げていますので、まずは各クラスに向かうようにしてみました。
 説明の必要はないかとは思いますが、
  1. 学年でループ
  2. クラスでループ
  3. クラスに属する児童でループ
 を行うことで、総当たり的にエージェント(児童)を生成する流れです。
 また、agent.GetComponent<AgentController>().className = belongs;の部分では、先に修正したAgentController.csのパラメータに、ループの過程で作成した所属クラスをセットしています。

* ループの中でインスタンス化を行うのはあまりよくないかもしれませんが、今回は触りとしての実装なのでご容赦下さい。
* Instantiateの第2引数に入れているVector3は、前回仮にエージェントを配置した初期位置を入れています。任意の座標に修正してください。

実行 | 修正

空のオブジェクトにスクリプトの割り当て

 シーン上に「空のオブジェクト」を作成し、スクリプトと同じ名前に変更後、先ほど作成したスクリプトを割り当てます。

Prefabをセット

 インスペクタに表示される「Agent」に、先ほどPrefab化したエージェントを、割り当てます。

実行

 1フロアにクラスがすべて配置されている場合は、このまま実行すれば各教室に移動するので簡易なシミュレーションはこの時点で可能かと思います。

修正

 私の作成したモデルのように複数階に渡って教室が配置されているプラン(相当土地に余裕がない限りはこちらが一般的ですね)の場合、学年によって行き先を階段にしてあげる必要があります。

 この記事では、「1Fに教室がある児童は教室へ、それ以外は階段に向うところまでを一旦実装して終わろうと思います。

 再度AgentController.csを修正します。

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

public class AgentController : MonoBehaviour
{
    public string className;
    public string step; // 階段のオブジェクトを格納 ← 新たに追加
    private int grade; // ← 新たに追加
    NavMeshAgent agent;  // NavMeshAgentコンポーネントを格納
    GameObject destination;  // 目的地のSphereを格納

    void Start()
    {
        grade = int.Parse(className.Split('-')[0]);

        // NavMeshAgentコンポーネントと目的地のオブジェクトを取得
        agent = GetComponent<NavMeshAgent>();

        /*******************************************
        * 以下を修正
        ********************************************/
        if (grade > 2)
        {
            destination = GameObject.Find(step);
        }
        else
        {
            destination = GameObject.Find(className);
        }


        if (agent.pathStatus != NavMeshPathStatus.PathInvalid)
        {
            // 目的地を指定(目的地のオブジェクトの位置情報を渡す)
            agent.SetDestination(destination.transform.position);
        }

    }
}

階段用のオブジェクトを作成
 階段の位置を管理するため、教室のときと同様にSphereを使って階段の位置を示すオブジェクトを作成します。

作成した階段位置を示すオブジェクトをセット
 AgentGeneratorを選択し、インスペクターから先ほど作成した階段の名前をセットします。

実行

 シーンを実行すると、1、2年生は教室へ向かい、それ以上の学年は階段へと向かうようになったかと思います。

 無事に動いているかと思います。うじゃうじゃと気色悪いですね。
 本記事はここまでとしますが、残作業としては
 * 教室にたどり着いた児童をシーンから削除
 * 階段にたどり着いたら対象階へワープ
 などがあります。

 そのあたりも投稿を希望する方がいらっしゃいましたら、コメントにてリクエストいただければと思います。
 リクエストが多数いただければ続きとして投稿させていただきます。

 今回も最後までご閲覧いただきありがとうございました。