🚆

Unityで交通シミュレーションをしたい ①鉄道と乗客を作る

2025/02/25に公開

はじめに

動いてるものって見てるだけでも楽しいですよね。

つい最近、VRに触れる機会があって、「これ自分が研究でやってる交通シミュレーションの可視化に使えるのでは?」と思いUnityの技術習得を始めました。普段はエージェントベースシミュレーションをしていますが、これとUnityとの相性もいいので、色々なことに役立つかもしれないという思いもあります。

最初は簡単な交通機関と乗客のみから作っていって、ゆくゆくはリアルに近いインフラを整備していって,オリジナルの仮想都市を作りたいと思っています。

現状できてるもの

一定の間隔ごとに電車が生成され、そこに乗客が乗車し目的地で下車するという、簡単な鉄道モデルを作ってみました。長方形のオブジェクトを電車、細長い棒を乗客に見立てています。

乗客が電車に乗車するとその乗客は「電車に乗車した」とみなされ、電車と一緒に移動します.電車は最初は緑色ですが、混雑状況に合わせて段々と赤色になっていきます。また、乗客の色も電車と同じ色に変化します。

UI上に仮想空間上の現在時刻を表示して、何時に何人乗客が乗下したかがわかるようにしています。

実装の流れ

今回はシミュレーション全体を管理するプログラムと、電車および乗客の各エージェントのルールを記述したプログラムを作りました。

シミュレーション全体管理用プログラムを作る

シミュレーション内の時間の流れやエージェントの生成/廃棄を制御するプログラムを作ります。もともとエージェントを空間内に配置しておく方法もありますが、今回は電車と乗客だけなのでエージェントの生成からプログラムからやりました。

電車の生成

SimulationManager.cs
void Update()
{
    // シミュレーション時間を進める
    simulationTime += Time.deltaTime * timeScale;
    if (simulationTime >= nextTrainTime)
    {
        SpawnTrain();
        nextTrainTime += trainInterval; // 次の電車の出発時刻を設定
    }
}

void SpawnTrain()
{
    Debug.Log($"電車を生成 (現在時刻: {Mathf.FloorToInt(simulationTime / 60)}{Mathf.FloorToInt(simulationTime % 60)} 分)");
    GameObject train = Instantiate(trainPrefab, trainSpawnPoint.position, Quaternion.identity);
    if (train == null)
    {
        Debug.LogError("電車の生成に失敗しました。");
        return;
    }

    trains.Add(train);
    Debug.Log("電車が正常に生成されました。");
    StartCoroutine(MoveTrain(train));
}

Update()関数内で、仮想空間内の時間の進行と、電車を生成を行います。

あらかじめ電車のプレハブ、および電車生成位置を表す空のオブジェクトをUnity画面上のInspectorから設定しておいて、同じ電車を同じ場所に生成するようにしています。生成された後は、その電車を移動させる関数を呼び出します。

オブジェクトを生成するInstantiate()の使い方は、以下の記事に詳しく記述されていたので、よければ参考にしてください。
https://qiita.com/Teach/items/c28b4fe5ca8dc4c83e26

電車の移動

SimulationManager.cs
IEnumerator MoveTrain(GameObject train)
{
    Debug.Log("電車の移動を開始します。");
    Vector3 start = train.transform.position;
    Vector3 target = cityPoint.position;
    float travelTime = 6f;
    float elapsedTime = 0f;

    while (elapsedTime < travelTime)
    {
        elapsedTime += Time.fixedDeltaTime;
        train.transform.position = Vector3.Lerp(start, target, elapsedTime / travelTime);
        yield return new WaitForFixedUpdate();
    }

    Debug.Log("電車が目的地に到達しました。削除します。");
    Destroy(train);
}

電車エージェントを仮想空間内の時間に合わせて進め、目的地に到達したら削除します。

こちらもあらかじめ電車の目的地を示す空のオブジェクトをUnity画面上のInspectorから設定しています。電車の座標は、あらかじめ決めておいた出発地ー目的地間の所要時間を用いて、毎分等速度で進むものとして計算しています。

IEnumerator()の使い方は、以下の記事で詳細に説明されています。
https://qiita.com/dj_kusuha/items/2048391d821cb94fa489

乗客の生成

SimulationManager.cs
void SpawnPassengers()
{
    for (int i = 0; i < totalPassengers; i++)
    {
        Vector3 randomOffset = new Vector3(Random.Range(-5, 5), 0, Random.Range(-5, 5));
        Vector3 spawnPos = stationPoint.position + randomOffset;

        if (NavMesh.SamplePosition(spawnPos, out NavMeshHit hit, 2.0f, NavMesh.AllAreas))
        {
            GameObject passenger = Instantiate(passengerPrefab, hit.position, Quaternion.identity);
            if (passenger == null)
            {
                Debug.LogError("乗客の生成に失敗しました。");
                continue;
            }
            passengers.Add(passenger);
            Passenger p = passenger.GetComponent<Passenger>();
            p.Init(cityPoint, this);
        }
        else
        {
            Debug.LogWarning("適切なスポーン位置が見つかりませんでした");
        }
    }
    Debug.Log("乗客の生成が完了しました。");
}

乗客エージェントを決められた位置の周辺に生成します。
詳細な行動は以下のPassenger.csに記載しています。

Passenger.cs
void Update()
{
    if (!isBoarded)
    {
        GameObject nearestTrain = FindNearestTrain();
        if (nearestTrain != null)
        {
            agent.SetDestination(nearestTrain.transform.position);
        }
    }
}

GameObject FindNearestTrain()
{
    float minDistance = float.MaxValue;
    GameObject nearestTrain = null;

    foreach (GameObject train in manager.trains)
    {
        if (train == null) continue;

        float dist = Vector3.Distance(transform.position, train.transform.position);
        if (dist < minDistance)
        {
            minDistance = dist;
            nearestTrain = train;
        }
    }
    return nearestTrain;
}

乗客エージェントは生成された後、電車に乗車するまでは近くの電車を探し、そこに向かっていきます。
近くの電車は、単純に自分自身と電車の距離の差を取ることで求めいて、NavMeshを用いることでそこに向かって動くようにしています。

NavMeshの使い方は、以下の記事を参考にさせていただきました。

https://zenn.dev/k1togami/articles/71519622146168

つまづいたこと

コードに関する問題は比較的簡単に解決できたものの、Unity初心者というのもあり、Unity独自の仕様に関する問題の解決に苦労しました。

解決に一番時間を使いました。

「電車は生成されているものの、乗客が生成されない」という問題に直面し、NavMeshが定義されていないことが原因であることが判明。インターネットやChatGPTで調べたところ「Navigation」ウィンドウからBakeボタンを押す、と記載されていました。しかし、Package Managerからの再インストールなどを試してもどこにも見つかりませんでした。

結果、Inspector内にありました。こんな簡単なところにあるのに気づかないとは,,

感想・今後やりたいこと

Unity初心者なのに、なぜ鉄道モデルから作り始めたんだろう、もう少しリンクとノードだけを表現したシンプルなモデルの方がよかったのでは?という思いもありますが、なんとなく使い方を理解することができたため、今後は挙動を改善していきたいです。

また,今後の発展としては以下のことを考えています

  • 交通機関をいくつか設定し、所要時間・料金に応じてエージェントに選ばせる
  • 運行本数を変化させ、全員が乗り切る時間を変化させてみる
  • リアルの座標・運行ダイヤ・人数(人口)などを使ってみる

これを使って具体的な政策検討までやってみたいな〜とも思っていますが、それはまだ先の話,,,

この記事を通して、少しでもエージェントベースシミュレーションやUnityに興味を持っていただければ幸いです。
ここまで読んでくださり、ありがとうございました🙇

(2025/3/15追記)乗客発生場所の修正

冒頭の画像では電車に乗車する場所と乗客の発生場所が同じだったため、乗客が電車に乗りに行く動きが少しわかりづらくなっていました。

そこで、乗客の居住地を駅から少し離れた場所に作成してみました。今回の画像を見ると、冒頭のものと比較して乗客の動きがわかりやすいと思います。

Discussion