Zenn
🚆

Unityで交通シミュレーションをしたい ②乗客に時刻選択をさせる

に公開

はじめに

技術習得をしていて一つできることが増えると、さらにその先をやってみたい!ってなりますよね。
私はしょっちゅう他のタスクに手がつけられない、というほどにハマってしまいます。

前回の記事では「一定の間隔で生成される電車に乗客が乗り、目的地で降りる」というシンプルな鉄道のモデルを作っていました。Unityでエージェントベースシミュレーションができることに感動したものの、もう少しリアルな社会現象に近づけたい!という思いもありました。

https://zenn.dev/wwaaoonn/articles/d4fa84691c6395

今回はその1歩として、以前作成した鉄道モデルを拡張して、乗客の出発時刻を分散させることで、よりリアルに近い動作をする交通シミュレーションにしたいと考えています。

現状できてるもの

乗客ごとに希望到着時刻を設定し、時間の経過とともに適切なタイミングで出発する通勤行動を再現するモデルを作成しました。前回と同様、長方形のオブジェクトを電車、細長い棒を乗客に見立てています。

出発するタイミングは、各乗客ごとに到着時刻を考慮したロジットモデルを用いて確率的に決定されます。発生場所については、出発時刻決定後にランダムで設定しています。

以前作成したモデルと比較し、8:00 ~ 9:00の電車の色が大きく変化しており、通勤ラッシュの波が生まれていることがわかります。

実装の流れ

今回は、前回のプログラムではゲーム開始直後に一斉に設定していた出発タイミングを、ループ毎に設定するように処理を変更します。また、乗客エージェントごとに異なる背景を持って意思決定を行うように、乗客エージェントにパラメータを変数します。

乗客エージェントへの変数の付与

パラメータを管理するクラスを作成

PassengerData.cs
public class PassengerData
{
    public float preferredArrivalTime; // 希望到着時刻
    public float estimatedTravelTime; // 予想移動時間
    public float suggestedDepartureTime; // 推奨出発時刻
    public bool hasDeparted = false; // すでに出発したかどうか

    public PassengerData(float preferredArrivalTime, float estimatedTravelTime)
    {
        this.preferredArrivalTime = preferredArrivalTime;
        this.estimatedTravelTime = estimatedTravelTime;
        this.suggestedDepartureTime = preferredArrivalTime - estimatedTravelTime;
    }
}

各乗客エージェントが持つ個別のパラメータを設定するクラスとしてPassengerDataを作成します。ここでは希望到着時刻や予想移動時間、出発済み判定のパラメータを持たせています。

前回乗客エージェントに付与するクラスとしてPassengerを作成しています。これとPassengerDataクラスをわざわざ分けたのは、データ管理とオブジェクト管理を分割することで、処理を効率化するためです。もし分割しない場合、ゲームオブジェクトとしてシーン上に必要ない時も乗客を生成する必要があるため、無駄な処理が増えるなどの問題があります。

乗客に関連する各クラスの役割を整理すると以下のようになります。

クラス名 役割
Passenger Unity上で動く「乗客オブジェクト」の行動を管理(移動・乗車処理など)
PassengerData 乗客ごとの「到着希望時刻」や「出発時刻」などのデータを管理
SimulationManager 乗客の生成・出発判定・シミュレーションの管理

各パラーメータの初期値を付与

SimulationManager.cs
private List<PassengerData> pendingPassengers = new List<PassengerData>();

void Start()
{
    GeneratePassengerData();
}

void GeneratePassengerData()
{
    for (int i = 0; i < totalPassengers; i++)
    {
        float preferredArrivalTime = Random.Range(480f, 540f); // 8:00 - 9:00
        float estimatedTravelTime = Random.Range(30f, 50f);
        PassengerData data = new PassengerData(preferredArrivalTime, estimatedTravelTime);
        pendingPassengers.Add(data); // まだ出発していない乗客リストに追加
    }
}

シミュレーション開始時に、先ほど作成したPassengerDataクラスに基づき、すべての乗客を生成します。

今回は、各乗客エージェントのパラメータとして、理想到達時刻preferredArrivalTimeを8:00 ~ 9:00の間で、予想移動時間estimatedTravelTimeを30~50分の間でランダムで設定しています。これにより、出発時刻にある程度ばらつきを持たせ、現実に近い振る舞いをさせます。

生成した乗客エージェントをpendingPassengersに格納することで、まだ出発していない乗客一覧を作成しています。

乗客エージェントを出発させる

SimulationManager.cs
void Update()
{
    // まだ出発していない乗客リストをスキャン
    foreach (PassengerData passenger in pendingPassengers)
    {
        if (!passenger.hasDeparted && ShouldSpawnPassenger(simulationTime, passenger))
        {
            SpawnSinglePassenger();
            passenger.hasDeparted = true; // 出発済みに設定
            spawnedPassengers++;
    
            // もし既に全員出発していたらループ終了
            if (spawnedPassengers >= totalPassengers)
                break;
        }
    }
}

各ループ毎に、まだ出発していない乗客に対して出発するかどうかの判定を行います。

先ほど設定した乗客エージェントが格納されたリストpendingPassengersを読み込み、そのうち出発していない乗客に対して後ほど説明するShouldSpawnPassenger()の結果に応じて乗客を空間内に生成します。

この処理を、全員が出発済みになるまで各ループごとに繰り返し実行します。

始業時刻選択アルゴリズムの作成

ここからは、乗客エージェントが時刻を選択するアルゴリズムを作成し、Unityに実装します。

ロジットモデルとは

ロジットモデル(Logit Model)とは、人の行動を数理的に記述する方法の一種で、個人が「複数の選択肢の中から各選択肢を選ぶ」という行動を、確率的に決定するモデルです。「電車とバス」「りんごとバナナ」「MacとWindows」のように、並列の選択肢であれば広く用いることができ、研究においても交通やマーケティング、都市計画などの多くの応用分野で用いられています。

研究以外でも、ゲーム開発においてはキャラクターの意思決定のリアリティを向上させる手段としても活用できます。例えば、キャラクターのAIがAまたはBの選択をとるときに、それぞれの行動確率の設定に使うことができます。

詳細は以下の記事で説明されているので、参考にしてみてください。

https://www.urban-data-lab.com/urban_logit/

出発時刻選択アルゴリズムを定義

通常の交通シミュレーションにおけるロジットモデルでは、複数の選択肢(例えば複数の電車やルート)の中から1つを選ぶ確率を定めますが、今回のケースでは「どの時刻に出発するか?」を決定するため、連続時間に適用可能な形に拡張する必要があります。

そこで、「希望出発時刻 TidepT_i^{\text{dep}} からのずれが小さいほど、選択確率が高くなる」 という仮定のもと、次のような確率分布を定義します。

Pi(t)=exp(βTidept)τTexp(βTidepτ)P_i(t) = \frac{\exp(-\beta |T_i^{\text{dep}} - t|)}{\sum\limits_{\tau \in \mathcal{T}} \exp(-\beta |T_i^{\text{dep}} - \tau|)}

ここで、各変数の意味は以下のとおりです。

  • exp(βTidept)\exp(-\beta |T_i^{\text{dep}} - t|)
    • 時刻 tt に出発する効用(Utility)
  • τTexp(βTidepτ)\sum_{\tau \in \mathcal{T}} \exp(-\beta |T_i^{\text{dep}} - \tau|)
    • すべての候補時刻 τ\tau に対する効用の総和
  • β\beta
    • このモデルの感度を表すスケールパラメータ
    • これが大きいほど確率が急激に変化

C#でアルゴリズムの実装

SimulationManager.cs
bool ShouldSpawnPassenger(float currentTime, PassengerData passenger)
{
    float beta = 0.3f; // 乗客の時間感覚のスケール
    float departureTime = passenger.suggestedDepartureTime;

    // 複数の出発時刻を考慮したロジット確率の計算
    float utility_current = -beta * Mathf.Abs(departureTime - currentTime);

    //  他の出発時刻(過去・未来)の効用も考慮
    float sum_exp_utility = 0f;
    for (float t = currentTime - 5; t <= currentTime + 5; t += 1f) // 過去5分~未来5分を考慮
    {
        sum_exp_utility += Mathf.Exp(-beta * Mathf.Abs(departureTime - t));
    }

    float probability = Mathf.Exp(utility_current) / sum_exp_utility;

    return Random.value < probability; // 確率的に決定
}

先ほど数式で記述したモデルをC#のコードに変換しています。

通常のロジットモデルを厳密に適応する場合は、考えうるすべての選択肢に対して効用を計算する必要があります。しかし今回は現在時刻の前後5分間のみを対象として計算しています。

理由は以下の2点です。1つは計算負荷の軽減です。すべての選択肢を毎回考慮していると、リアルタイムでのシミュレーションのパフォーマンスが大きく低下します。2つ目は乗客の現実の意思決定を、通常乗客はいつまでも待つわけではなく、ある程度の時間の範囲内で意思決定をすると考えられます。そのため、±5分という短い時間の範囲に限定して評価しても、十分に現実的な意思決定になると判断しました。

感想・今後やりたいこと

通勤ラッシュが再現されるだけでも、リアルの鉄道らしくなりますね。作っていて感動してしまいました 笑

今回は連続値を無理やりロジットモデルに適応していたため、より自然な決定方法(乗車する電車を選択する方法に変更するか、生存モデルなど連続値を扱えるモデルを用いる)に変更する必要がありそうです。

今後の改善・発展としては以下のようなことを考えています。

  • 出発時刻選択モデルに、料金や混雑度といった指標を加える
  • 交通機関を複数設定し、エージェントに利用する路線を選ばせる
  • 発生人数や運行本数を、実際の地域や鉄道路線に当てはめてみる

具体的な政策検討ができるレベルで交通行動を再現するのは難しいですが、いつかできることを願って発展させたいと思います。

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

Discussion

ログインするとコメントできます