サービスロボット導入シミュレーション Log#4 | Unity にファミレスを建て、配膳ロボットを走らせた
配膳ロボットは、店の中で何回止まり、止まるたびに何秒待たされるのでしょうか。配膳ロボットで店の人を1人減らせるかを試算した回(LOG351)では、ここを仮の数字で置いていました。店の間取りがないと計算できないからです。そこで、Unity(建物や人を3Dで置いて動かし、時間を測れるソフト。もとはゲームを作る道具)の中に、卓35・席122のファミリーレストランを建てました。配膳ロボット2台とスタッフ2人、それに入店してから会計して帰るまでのお客さんを動かしています。コードは Claude Code(Mac の中でプログラムを書いて動かす AI の道具)が書き、こちらは画面を見て「この店はおかしい」と言う役を受け持ちました。

建て始めてから区切るまでに、間取りを5回ほど作り直しました。壁ぞいの奥の席に人が入れない、レジのまわりに16人が固まる、お客さんが自分からロボットにぶつかっていく、といった、実際の店ではありえない動きが画面に次々と出たからです。そして、店の形をひととおり整えて2時間分を回した1回目では、ロボット2台の往復86回のうち76回(88%)で、ロボットが60秒以上動けなくなりました。料理ができてから席に届くまでは平均12.7分で、同じ条件の試算の0.67分の約19倍です。
原因を追うと、ロボットの性能より、店と人の側にありました。お客さんが椅子を引いて通路にはみ出したのは、回している間に497回(1回30秒。この長さは仮の値)。ロボットが細い通路の入口で順番を待った時間は、合わせて約98分でした。Unity で出てくる数字は、ロボットの性能というより、置いた店と人の動きの結果です。店と人が本物らしくなるまでは、その数字を試算に戻せません。 この回は、その「本物らしい店」を作るところまでの記録です。
| やったこと | なぜ | どうなったか |
|---|---|---|
店を、卓を格子に並べた形から、厨房・パントリー・レジ・トイレのあるファミレスの形に建て直した | 奥が行き止まりの通路が3本ある形は、実在の店に近くなかった | 35卓・122席。すべての席に通路から入れることを確かめた |
| 時間の速め方を3通り試した | 2時間分を何十回も回すには、等倍では遅すぎる | 1コマの時間を固定する方法にした。等倍との差は中央値で1〜3% |
お客さんに、入店から退店までの流れを持たせた | 通路をずっと歩き回る作りでは、歩く人が実際の店より多すぎた | 来るペースと食事の時間を、試算とそろえた |
人がロボットをよけるようにした | 細い通路で、人がロボットを両側からはさむ場面が出た | よけない人を、1回の行動につき2%(仮)だけ混ぜた |
床の水色を、ロボットの体が入れる範囲で塗った | ロボットの中心が通れる範囲で塗ると、細い通路が1本の線に見えた | 卓と椅子のきわまで水色になった |
レジの奥に、スタッフが入る場所を作った | 会計のスタッフが、お客さんと同じ側からレジに立っていた | スタッフはレジ台の奥に立ち、お客さんは手前に並ぶ |
作り直すたびに録った画面の動画を、5本載せています。ロボットが止まる回数を条件ごとに測り、試算と並べるのは次の回です。
ここから下は作業ログです。試算のどこを測るか、店の作り直し、1回目の結果、床の塗り分けとレジの直し、区切った時点で残っていることを順に書き、そのあとに出力・設計書・依頼文・コードの全文を載せています。
作業ログ ― SimPy の試算で仮置きにした値のうち、結果を動かすのは3つだった
LOG351 の試算は SimPy(Python で「何が、いつ、どの順番で起きるか」を再現する道具)で組んでいます。料理ができる時刻、スタッフとロボットの手が空く時刻、お客さんが来る時刻を並べて、待ち時間を数えるものです。ロボットが通路で止まる・動けなくなるところは、店の形がないと計算できないので、次の仮の値を入れていました。
| 仮の値 | 中身 | 試算で入れていた値 |
|---|---|---|
| robot_speed | ロボットが実際に走れる速さ | 0.5 m/秒 |
| pause_rate | 1往復のうち、途中で一時停止する割合 | 30% |
| pause_s | 一時停止の長さ | 3〜10秒 |
| stuck_rate | 人が動かしに行かないと動けなくなる割合 | 1% |
Unity を作る前に、この4つを1つずつ動かして、試算の結論が変わるかを確かめました(simulation/sensitivity/sweep.py)。標準ファミレスでスタッフを3人から2人に減らし、ロボット2台を入れた条件で、料理ができてから席に届くまでの時間を見ています。各条件40回の平均です。
| 振ったもの | 標準ファミレス(ロボットなし 58秒) | 焼肉食べ放題(ロボットなし 77秒) |
|---|---|---|
| 仮の値のまま | 40秒 | 59秒 |
| 一時停止の割合 0% → 90% | 38 → 45秒 | 62 → 65秒 |
一時停止の長さ 3〜10秒 → 30〜60秒 | 40 → 54秒 | 59 → 76秒 |
動けなくなる割合 1% → 20% → 30% | 40 → 52 → 58秒 | 59 → 77 → 82秒 |
ロボットの速さ 0.5 → 0.3 m/秒 | 40 → 64秒 | 59 → 91秒 |
止まる回数は、0%から90%まで動かしても7秒しか変わりません。結果を動かすのは、1回止まったときの長さ、動けなくなる割合、実際に走れる速さの3つで、どれも悪い側に振れると、ロボットを入れて料理が早く届くという利点が消えます。そこで Unity では、止まる回数ではなく、この3つを測ることにしました。
| Unity で測るもの | 数え方 | 試算のどこに戻すか |
|---|---|---|
| 実際に走れる速さ | 走った距離 ÷(往復にかかった時間 − 止まっていた時間) | robot_speed |
| 止まっていた長さ | 1回止まってから、自分で動き出すまでの秒数の分布 | pause_s |
| 動けなくなった割合 | 1往復のうち、60秒以上動けなかった往復の割合(人が動かしに行く扱い) | stuck_rate |
| 止まった割合(参考) | 1往復のうち、1回以上止まった往復の割合 | pause_rate |
通路の幅(0.8 m・1.0 m・1.2 m)、お客さんが来るペース(1時間に25・35・45組。試算と同じ)、ロボットの台数(1台・2台)を組み合わせた18の条件で測り、試算に戻して LOG351 の表を回し直す、という段取りです。この回は、その測る場所になる店を作るところまでです。
Unity の中を、店・ロボット・人・実験の4つのフォルダに分けた
作る前に、Claude Code に頼むときの分け方を決めました。店の形を直すときにロボットのファイルを開かずに済み、ロボットの止まり方を変えるときに人のファイルに触らずに済むようにするためです。
Assets/ServiceSim/
├── Scenes/ServiceSim_Family.unity … 店・ロボット・人・実験を置くだけ
├── Store/ … 店(卓の並び、通路の幅、厨房の位置。床・壁・卓・椅子を並べる)
├── Robot/ … ロボット(大きさ、速さ、止まる距離、動けなくなったとみなす秒数)
├── People/ … 人(スタッフ、お客さん、座っているお客さんが引く椅子)
├── Experiment/ … 実験の回し方と記録(条件、乱数の種、出力先)
├── Service/ … 店の仕事(料理・案内・会計・片付けの順番待ち、組の流れ)
└── Editor/ … Unity のメニュー(Setup・Build Store・Run One など)
数字(通路の幅、止まる距離、歩く速さ、人数など)はコードに書かず、ScriptableObject(Unity のインスペクターで値を変えられる設定ファイル。拡張子は .asset)に置いています。数字を直すときは .asset だけを変え、回すたびにその値を conditions.json に書き出すので、どの値で回した結果かがあとで分かります。
ロボットは実在の機種の形を使わず、直径0.5 m・高さ1.2 m の円柱にしました。止まり方は、実機の多くが前の距離センサーで人を見て止まる動きを、次のように単純にしています。
| 決まり | 値(仮) |
|---|---|
進む方向の前に人か物があれば止まる | 0.6 m 以内 |
| 前が空いたら動き出す | 1秒空いたら |
止まったまま待ったら、別の道を探す | 10秒 |
動けなかったら「動けなくなった」と記録し、人が助けに来る扱いにする | 60秒 |
| 最高速度 | 0.6 m/秒 |
道を探すのには、Unity の NavMesh(床のうち歩ける部分を三角形の網で表した地図)と NavMeshAgent(その地図の上で道を探して進む部品)を使っています。NavMeshAgent には、ほかの体を自動でよける機能もついていますが、実機のよけ方とは違うので、ロボットには使っていません。道を探すところだけを使い、止まる・動き出すは上の決まりでコードに書きました。
Claude Code に頼むときの決まりも先に書いています。このプロジェクトには mechArm(ロボットアーム)のシーンが同じ Unity の中にあるので、ServiceSim のフォルダの外は1バイトも変えないこと、Unity の操作は人がやるので押す順番を書いて止まること、変更の記録(Unity Version Control のチェックイン)は結果を見てからにすること、です。
最初の店は卓を格子に並べた形で、床が赤紫に塗られた
Claude Code が最初に建てた店は、厨房の受け渡し口を南西の角に置き、そこから太い通路が1本北へ伸び、細い通路が3本東へ枝分かれする形でした。卓は細い通路の両側に6卓ずつで、店の内側は 9.7 m × 16.6 m です。
最初に開いた画面は、床も卓も赤紫一色でした。

赤紫は、Unity が「この素材の描き方が分からない」ときに出す色です。調べると、このプロジェクトの描画の設定は URP(Unity の新しい描画の仕組み)を指していましたが、その設定ファイルの中身が読めなくなっていて、実際には古い描画の仕組み(Built-in)で描いていました。Claude Code は URP 用の素材を作っていたので、描けずに赤紫になっていたわけです。設定ファイルを直すと mechArm のシーンの見た目まで変わるおそれがあるので、設定には触らず、ServiceSim の側で「いま実際に使っている描画の仕組み」を調べて、それに合う素材(Standard)を選ぶようにしました。

色が付くと、店の形のおかしさが見えてきました。
通路とか狭すぎじゃない?
あとこんな高いパーテーションのお店あまり見たことない(回遊性があるならいいけど、奥に向かって3部屋というのは?)
細い通路は奥が行き止まりで、ロボットは同じ通路を行って戻るしかありません。高い仕切りで3つの部屋に分かれたような店は、ファミレスとしてはあまり見かけません。
実在のファミレスの図面から、場所の並びと寸法の目安だけを借りた
こういったので参考になりますか?厨房やウェイター、ウェイトレスの作業場、トイレなども考慮した間取りにしないと現場感が出ないですよ?
実際のファミレスの図面を何枚か見て、どの場所がどこに並ぶかと、寸法の目安だけを借りて作り直しました。図面そのものは写さず、記事にも載せていません。作り直した店(StoreLayout_Family)は次の形です。
- 左に客席、右に厨房。厨房と客席のさかいに、奥から「料理の受け渡し口」「パントリー(水・おしぼり・カトラリーを置く棚)」「下げた食器の返却口」が並ぶ
- その前を、幅1.6 m の太い通路が南北に通る。ロボットは受け渡し口の前から出て、この通路を通って卓へ行く
- 入口(風除室つき)を入ったところに、待合とレジ。トイレは入口の横
- 卓は35。左の壁ぞいのボックス席8、上と下の壁ぞいの卓、中央の島18。島のまわりはぐるりと回れて、行き止まりがない
- 背中合わせの席のあいだの仕切りは、ボックス席の背もたれの高さ(1.1 m)
太い通路には、パントリーに立ち寄るスタッフ、返却口へ向かうスタッフ、レジやトイレへ向かうお客さんが集まります。人とロボットが交わる場所を、実際の店と同じくここに集めました。

細い通路の幅は、0.8 m・1.0 m・1.2 m の3段階で切り替えられるようにしました。配膳ロボット BellaBot が通れる幅は、ソフトバンクロボティクスの案内では70 cm 以上、シャープの案内では約80 cm です。客席のあいだの通路の目安は60 cm 以上、スタッフがよく通る場所は80 cm 以上と言われます(法律で決まった値ではありません)。0.8 m は、ロボットがぎりぎり通れる店にあたります。
受け渡し口から35卓までの道のりは、細い通路 1.0 m のとき 1.84〜18 m(平均 9.78 m)です。試算では道のりを 6〜22 m と置いていたので、35卓のうち28卓がその範囲に入ります。
時間の速め方を3通り試し、1コマの時間を固定する方法にした
店の中の2時間を18条件、乱数の種を変えて5回ずつ回すと、等倍では180時間かかります。Unity で時間を速めるやり方は2つあります。時間の進みを10倍にする方法(timeScale)と、1コマで進む時間を決めてしまう方法(captureDeltaTime)です。同じ条件(細い通路 1.0 m・ロボット2台・20往復)を、等倍・10倍・1コマ0.02秒固定の3通りで回して比べました。
| 回し方 | 倍速 | 1コマの時間 | 1コマで進む距離の最大 | 走れる速さ(中央値) | 止まっていた秒数(中央値) |
|---|---|---|---|---|---|
| 等倍 | 1.00 | 中央 0.0017秒・最大 0.3333秒 | 0.195 m | 0.550 m/秒 | 77.6秒 |
| 10倍 | 9.98 | 中央 0.0189秒・最大 2.8226秒 | 0.330 m | 0.535 m/秒(−3%) | 77.2秒(−1%) |
| 1コマ固定 0.02秒 | 9.75 | ずっと 0.0200秒 | 0.012 m | 0.546 m/秒(−1%) | 79.9秒(+3%) |
10倍にすると、パソコンが一瞬引っかかったときに2.8秒ぶんの時間が1コマで進み、ロボットが0.33 m を一息に飛びます。ロボットの直径の半分以上です。等倍でも引っかかりは出ていて、等倍だから正確というわけではありませんでした。1コマ固定なら、進む時間がいつも同じで、速さは10倍とほぼ同じです。中央値は等倍と1〜3%の差に収まったので、1コマ固定で回すことにしました。

この段階では、ロボットの往復の75%が「動けなくなった」になりました。止まっていた時間の57%は、椅子が原因です。ロボットが卓の前で止まる点が、通路の真ん中から卓の側へ0.15 m ずれていて、引かれた椅子から0.10 m しか離れていませんでした。止まる点を通路の真ん中に直しています。
そのあと、2時間を1本8分ほどで回せるように、1コマを0.05秒にしました(店の中の135分を481秒で回し、約17倍速)。0.02秒と0.05秒で結果が変わらないかを、乱数の種を5つ変えて確かめる検査は、まだ回していません。
お客さんに、入ってから帰るまでの流れを持たせた
第2段階のお客さんは「歩く客4人が、卓・レジ・トイレ・ドリンクバーのあいだを、ずっと行き来する」作りでした。通路を歩く人が実際の店よりずっと多く、卓に戻ったお客さんが座らずに通路に立ち続けることもありました。どれも、ロボットの止まる回数と長さを大きく見せます。

お客さんの動きにある程度規則性を持たせることは可能なの?
そこで、お客さん1組ごとに次の流れを持たせました。
- 入店する。席が空いていなければ待合で待つ(待合に立てるのは8人まで。それより多い組は、店の外で待っている扱いにして、待ち時間だけを記録する)
- スタッフの案内で卓へ行き、座る
- 注文して食べる。そのあいだに、ときどきドリンクバーやトイレへ行き、戻って座る
- 食べ終わったら、1人がレジで会計する
- 全員で店を出る。スタッフが片付けて、卓が空く
来るペース、食事の時間、料理の数、料理をロボットとスタッフのどちらが運ぶかの決まりは、試算(haizen_sim.py)とそろえました。ロボットは料理を頼んだ卓にだけ行き、1回の往復で最大3卓ぶんを載せて、近い卓から順に回ります。試算にない値は仮で置いています。
| 値 | 仮の値 |
|---|---|
| 1組の人数 | 1人20%・2人35%・3人20%・4人25% |
| ドリンクバー | 1人平均1.5回・1回20秒 |
| トイレ | 1組平均0.3回・1回180秒 |
| 回し始めの店 | 卓の80%が食事中(座ったばかり・食べている・食べ終わりかけを混ぜる)。最初の15分は数えない |
この形で最初に回した回は、卓がほとんど回りませんでした。待っている組が0・4・7・10・16・20と増え続け、入店を待った時間は平均48.6分です。原因は「あきらめる道」がなかったことでした。ドリンクバーから戻れないお客さんが1人いると、その組がいつまでも食事中のままで、卓が空きません。お客さん・スタッフ・組のそれぞれに、60秒たっても着けなければあきらめて戻る、会計や片付けで10分止まったら卓を空ける、といった道を足しています。あきらめた回数は give_ups.csv に1回1行で書き、多ければ人が通れない店になっているしるしとして読みます。
壁ぞいの4人席は、奥の席に人が入れなかった
上と下の壁ぞいの卓は、最初は4人席(壁側のベンチ2人+通路側の椅子2脚)でした。
左の席人が奥側の席に入れないね。無理して4人席じゃなくてもいいんじゃない?
上と下の席は横から入れるだろうけど、真ん中の席は入れないね
卓と卓のあいだを仕切りでふさいでいたので、壁側の奥の席に座る人が、卓の横から入れなかったのです。

壁ぞいの卓は2人席(壁側のベンチ1人+通路側の椅子1脚)に変え、卓のあいだに人が通れるすき間(いちばん狭いところで 0.75 m)を空けました。いちばん狭い細い通路 0.8 m のときに入る数にそろえて、上に6つ・下に3つです。ボックス席は、ベンチの奥行きを 0.55 m(座面0.45 m+背もたれ0.1 m)、1つの幅を 1.85 m に広げました。最初は座面が実質0.35 m で、4人掛けとしては狭すぎたためです。

1〜2人の組は空いている2人席を先に使い、なければ4人席へ。3〜4人の組は4人席だけを使います。試算には卓の大きさの区別がないので、入店待ちが試算とずれる理由の1つになります。
店を作るたびに、Build Store(店を組み立てるメニュー)が、全35卓の席の前にある106か所の立つ点に、人が通路から入れるか、細い通路を1本ずつふさいでもほかの全卓に届くか(行き止まりがないか)を確かめて、報告に書くようにしました。
ドリンクバーは、最初は1 m ほどの台1つでした。ファミレスのドリンクバーとしては狭すぎるので、長さ3.6 m(機械4台分)・奥行き0.7 m のカウンターにし、前に4人が立てる場所と、端に3人が並べる場所を作っています。35卓を保ったままでは今の店の大きさに入らなかったので、下の壁を外へ1.3 m 出して、くぼみに置きました。
人が、自分からロボットに当たりに行かないようにした

人からロボットに当たりに行くような行動は現実ではあり得ない(あるとしたらわざとか、子供とか)ので、人はロボットを避けるように設定してください(たまに当たるくらいはあるだろうけど…頻繁はおかしい)
それまでの人は、ロボットを気にせずに歩いていました。人がロボットに向かって歩けば、ロボットのセンサーは前に人がいると判断して止まります。これでは、実際の店よりロボットが止まる回数が増えます。人の動きに次の決まりを足しました。
- 細い通路の中にロボットがいたら、通路に入らずに入口で待つか、5 m 以内の回り道を通る
- 通路の中で向かい合ったら、人のほうが交差点や卓のすき間へ下がる
- 広い通路では、ロボットから0.3 m 以上空けてすれ違う
- これを守らない不注意な人を、1回の行動につき2%(仮)だけ混ぜる
人どうしがすれ違えないときは、体を横に向けて細くなり(半径0.20 m → 0.12 m)、それでも抜けられなければお客さんのほうが下がります。
レジのまわりに、お客さんが16人固まった
あとレジに異常に溜まりすぎ。全体の流れの計算がちゃんとできていない?

次の節の1回目(11時3分の回)の記録では、レジのまわりに最大16人、平均10.5人がいました。原因は2つです。待合・レジ・入口の3つを、奥行き1.6 m・広さ4.8 m² の帯に詰めていたこと。もう1つは、会計のスタッフがお客さんと同じ側からレジに立っていたことです。待っているお客さん、会計するお客さん、帰るお客さん、店に入ってくるお客さんの流れの中に、スタッフが混ざっていました。

レジの後ろに隙間を作って、スタッフはその隙間に入るようにした方がいいね
この直しを入れる前の状態を、次の節の1回目として回しました。
店を整えた1回目は、ロボットの往復の88%で動けなくなった
細い通路 1.0 m・1時間に35組(普通の混み具合)・ロボット2台・乱数の種0・1コマ0.05秒で、店の中の135分(最初の15分は数えない)を回しました。試算(haizen_sim.py)の同じ条件(標準ファミレス・スタッフ2人・ロボット2台・35組/時・料理を運ぶだけ・最大3卓)と並べます。試算は40回の平均、Unity は1回だけです。
試算(SimPy・40回の平均) | Unity(1回) | |
|---|---|---|
料理ができてから席に届くまで(平均) | 0.67分 | 12.68分 |
| 入店を待った時間(平均) | 13.98分 | 16.07分 |
| 2時間で店を出た組 | 69.45組 | 44組 |
| ロボットが運んだ料理の割合 | 88% | 16% |
| 動けなくなった往復の割合 | 1%(仮の値) | 88%(86往復のうち76) |
ロボットが止まった313回を、原因で分けるとこうなります。原因の分け方は、店でロボットを観察するために作ったシート(LOG353)の選択肢にそろえています。
| 原因 | 回数 | 合計秒数 | 1回の平均 | うち動けなくなった |
|---|---|---|---|---|
| 通路の順番待ち | 75 | 5,855.6秒 | 78.1秒 | 0 |
| 椅子・荷物・ベビーカー | 80 | 4,185.7秒 | 52.3秒 | 63 |
| もう1台のロボット | 35 | 999.3秒 | 28.6秒 | 9 |
| 人が立ち止まっていた | 23 | 582.9秒 | 25.3秒 | 4 |
| 分からない | 73 | 169.9秒 | 2.3秒 | 0 |
| スタッフとすれ違った | 23 | 60.8秒 | 2.6秒 | 0 |
| お客さんとすれ違った | 4 | 33.1秒 | 8.3秒 | 0 |
動けなくなった76回のうち63回は椅子です。お客さんが椅子を引いたのは497回で、きっかけは、戻って座る139回・ドリンクバーへ行く135回・食事中130回・座る40回・会計35回・トイレ10回・退店8回でした。椅子は0.3 m 引かれて30秒そのままになります(どちらも仮の値)。細い通路 1.0 m にロボット(直径0.5 m)が止まると、左右に0.25 m ずつしか残りません。そこに椅子が0.3 m はみ出すと、ロボットはその卓の前の点に着けなくなります。
料理が届くまでの12.68分の中身も分けました。スタッフが運んだ92皿は、受け渡し口で平均13.0分待っていて、運ぶのにかかったのは0.69分です。ロボットが運んだのは18皿だけで、ロボットに載せたのに届けられずに受け渡し口へ戻した回数は、延べ268回ありました。ロボットが動けないので料理がスタッフに回り、スタッフ2人は回している時間の99.5%が何かの仕事中(料理53%・案内29%・呼び出し10%・会計8%)でした。
この1回の数字は、ロボットの性能より、仮で置いた椅子の引き方とレジまわりの作りを映しています。 椅子を何回・何 cm・何秒引くかは、店で数えないと分かりません。この数字はまだ試算に戻さず、店と人の作りを直してから測り直します。
この前に回した10時33分の回の出力は、名前を変える前に次の回で上書きされて残っていません(レジの待ち時間の中央値388秒、食べ終わった68組に対して会計できた35組、という数字だけが報告に残っています)。
床の水色を、ロボットの中心ではなく体が入れる範囲で塗った
床の水色の部分は何を表現しているんだっけ?
Unity の画面で床に出ていた水色は、NavMesh の表示でした。NavMesh は「ロボットの中心が行ける範囲」で作られています。ロボットの半径は0.25 m なので、壁や卓から0.25 m 以内は、中心が入れない場所として網から外れます。幅1.0 m の細い通路なら、水色は真ん中の0.5 m だけです。

ロボットの本体が動ける範囲で水色の部分作らないと、見る人が勘違いするね
この画面を初めて見た人は「ロボットはこの細い帯しか通れない」と読みます。実際には、ロボットの体は帯の外側へ0.25 m ずつはみ出して、通路の幅いっぱいを使っています。そこで、人に見せる絵では NavMesh の表示を使わず、床を別に塗ることにしました。
- ロボットの体が入れる床(水色):ロボット用の NavMesh を、外へ0.25 m 広げた範囲
- 人だけが入れる床(薄い黄色):人用の NavMesh(半径0.2 m)を0.2 m 広げた範囲から、水色の範囲を引いたところ
こう塗ると、すき間が0.5 m より狭いところだけが水色にならず、ロボットが通れない場所がそのまま絵に出ます。 下の図で、通路の幅を動かして確かめられます。

水色が濃くて見た目がきついね
動いている方はちょうどいいね
Build Store で塗った水色は不透明で、家具の色まで消していました。再生中の画面の、光と影が乗った薄い水色のほうが見やすいので、色はそちらにそろえることにしています。ただ、再生中の画面に出ている水色は、Unity が自分で出す NavMesh の表示(中心が行ける範囲)のままで、塗った形とは違います。これも次に直します。
画面に何を出すかは、清水秀樹さんが LinkedIn に投稿していた、Unity で隅田川を再現した操船シミュレーターの画面を参考にしました。建物も船も白い箱なのに説得力があるのは、出している数字(橋の下の高さと船の高さ、次に曲がる場所、時刻と倍速)が本物だからです。こちらも、画面に出す数字は記録(trips.csv・stops.csv)に書く数字と同じものだけにする、と決めました。
レジの奥に、スタッフだけが入る場所を作った
レジ台をはさんで、手前(南)をお客さん側、奥(北)を店員側に分けました。店員側は 0.8 m × 1.2 m で、太い通路からだけ入れます。北は壁、東はトイレの壁、南はレジ台なので、お客さんの列や待合の中を通らずに入れます。
店員はレジの奥側に立たないとね
最初の版では、店員側はできていても、スタッフがその中まで入れませんでした。

店員レジの奥に入れてない!惜しい!
人もまだロボット用(半径0.25 m)の NavMesh を歩いていたのが原因です。半径0.25 m で作ると、壁から0.25 m 以内は歩ける面になりません。最初の店員側は奥行き0.8 m で、入口の歩ける幅は 0.8 − 0.5 = 0.3 m しか残らず、人(直径0.4 m)が入れませんでした。
店員側の奥行き × NavMesh の半径 | 入口の歩ける幅 |
|---|---|
0.8 m × ロボット用 0.25 m | 0.30 m(人は入れない) |
| 0.8 m × 人用 0.2 m | 0.40 m(ぎりぎり。網の細かさ0.05 m で消えやすい) |
| 1.2 m × 人用 0.2 m | 0.80 m |
1.2 m × ロボット用 0.25 m | 0.70 m |
人とスタッフは人用(半径0.2 m)の NavMesh を歩くようにし、店員側の奥行きは、入口に0.7 m 以上の歩ける幅が残る1.2 m にしました。会計のたびに、スタッフが立つ点から実際に立った位置までの距離を記録し、0.1 m を超えた回数を summary.json に出します。

レジには人は入れるようになった。
直したあと、店員側にスタッフが2人入る場面が出ました。レジの並ぶ点が3つあるので、会計の仕事が同時に2つ出ることがあり、スタッフ2人が両方を取っていたのです。レジは1台なので、会計の仕事は同時に1つしか取れないようにしました。
NavMesh が2枚になり、道を引くたびに人かロボットかを指定した
人用の NavMesh を足すと、Unity の Console(作業の記録や警告が出る欄)に、黄色い警告が19件出ました。
CalculatePath() could not determine precisely which agent type should move from Source position ( 10.067490, 0.025000, 11.900000 ) to Target position ( 1.900000, 0.025000, 12.025001 ). Use the filter parameter of CalculatePath
NavMesh が人用とロボット用の2枚になったのに、道を引くときにどちらの網で引くかを指定していない、という警告です。指定がないと、ロボットの道を人用の網で引いてしまうことがあります。人用の網はロボット用より0.05 m ずつ広いので、ロボットの体が通れないすき間を通る道になり、止まる回数がずれます。
ロボットの行き先を決めるところと Build Store の確かめで、古い形(網の種類を指定しない形)で呼んでいたところを、道を引く相手を指定する形(NavMeshQueryFilter)に直しました。Build Store では、受け渡し口から全35卓へ道を引き、道の193個の点がすべてロボット用の網の上にあることを確かめています。
区切った時点で、直っていないこと
直ってないね。一回ここで区切ろう(キリがない)
区切った時点の店で、直っていないことと、まだやっていないことです。
| 項目 | いまの状態 |
|---|---|
| 場所の名前 | 13個を1つずつ出すようにしたが、Scene ビューでは縦書きの大きな文字が重なって読めない。真上のカメラの画面では横書きだが、名前どうしが重なっている |
| Console の警告 | 道の指定を直したあとの Build Store でも、黄色い警告が8件残っている。中身はまだ見ていない |
| 細い通路の歩ける幅の測り | 全10本で「焼いた面のいちばん狭いところ 0 m」と出る。道は引けているので、測る側のコードが正しく動いていない |
| 人だけが入れる床(薄い黄色) | 0 m²と出て、塗られていない |
| 再生中の床 | Unity の NavMesh の表示(中心が行ける範囲)が出ていて、塗った形と違う |
| Run One の出力 | 同じ条件で回すと前の回を上書きする。10時33分の回はこれで消えた |
| 止まる点の比べ | 止まる点を通路の真ん中(0)と卓寄り(0.15 m)で1回ずつ回して比べる計測は、まだ回していない |
| 1コマ0.05秒の検査 | 0.02秒と0.05秒で結果が変わらないかの検査は、まだ回していない |
| 変更の記録 | 段階1の終わり(cs111)より後の変更は、結果を見てからチェックインする決まりで、まだしていない |

場所の名前と警告から直し、そのあと止まる点の比べを回して、試算と並べます。
データの置き場所
| データ | 置き場所 |
|---|---|
| Unity のコード・設定 | Mac の /Users/shimazueiichi/Robotics_UX/Robotics_UX_New/Assets/ServiceSim/(Unity Version Control で管理) |
| 段階2の出力と動画 | KIOXIA の r2r2r-data/service_sim/unity/20261021_stage2_family_v0/ |
流れを持たせた最初の版の出力と動画 | KIOXIA の r2r2r-data/service_sim/unity/20261021_one_family_v0_v2_run1/ |
直す前の1回目(11時3分の回)の出力と動画 | KIOXIA の r2r2r-data/service_sim/unity/20261021_one_family_v0_v3_off000_before_fixes/ |
| レジの直しのあとの動画 | KIOXIA の r2r2r-data/service_sim/unity/20261021_one_family_v0_v3_register_back/ |
設計書・依頼文・Claude Code の報告・画面 | iCloud の サービスロボット導入シミュレーション/unity/ |
| 試算(SimPy) | iCloud の サービスロボット導入シミュレーション/simulation/ |
実行した操作と出力
Unity の操作は、Unity の上のメニュー ServiceSim から行います。店を組み立てるときは Setup(.asset に書かれていない項目を、いまの初期値で書きそろえる)→ Build Store(店を組み立て、NavMesh を焼き、確かめの報告と真上の画面を書き出す)の順に押します。1回だけ回すときは Run One、見るために回すときは Run One の「見る用:字幕あり・描画あり(1倍速)」を押します。
Build Store の報告(store_report.txt・2026-10-21 12:09:43 に書き出したもの)
ServiceSim 店の報告(v1 ファミレス型)
作った日時: 2026-10-21 12:09:43
Unity: 2022.3.62f3
run_name: family_v0
描画方式
GraphicsSettings.currentRenderPipeline: null(Built-in)
GraphicsSettings.defaultRenderPipeline: null(Built-in)
QualitySettings(レベルごとの上書き): 6 レベル
[0] Very Low: 上書きなし(既定を使う)
[1] Low: 上書きなし(既定を使う)
[2] Medium: 上書きなし(既定を使う)
[3] High: 上書きなし(既定を使う)
[4] Very High: 上書きなし(既定を使う)
[5] Ultra: 上書きなし(既定を使う)
いま選ばれている品質レベル: 5
判定: Built-in
メモ: Built-in として扱います。URP を使うつもりなら、Graphics Settings に入れた
パイプラインアセットが読めていない可能性があります(スクリプト参照切れなど)。
ServiceSim は描画方式そのものを変えません。
使うシェーダー: Built-in(currentRenderPipeline が null)→ Shader.Find("Standard")
いまの設定
店の形: StoreLayout_Family(ファミレス型(客席+厨房+パントリー))
細い通路の幅: 1 m
通路の譲り合い: あり
1往復の打ち切り: 600 秒
店の中身
卓: 35
壁際ブース 8 / 壁沿いの卓 9 / 島の卓 18
2人席 9 / 4人席 26 → 席の合計 122
通路: 12(うち細い通路 10)
[0] MainAisle 幅 1.6 m(太い・横通路)
[1] SouthAisle 幅 1 m(細い)
[2] CrossAisle 幅 1.2 m(太い・横通路)
[3] NorthAisle 幅 1 m(細い)
[4] WestAisle_S 幅 1 m(細い)
[5] WestAisle_N 幅 1 m(細い)
[6] ColumnAisle01_S 幅 1 m(細い)
[7] ColumnAisle01_N 幅 1 m(細い)
[8] ColumnAisle02_S 幅 1 m(細い)
[9] ColumnAisle02_N 幅 1 m(細い)
[10] ColumnAisle03_S 幅 1 m(細い)
[11] ColumnAisle03_N 幅 1 m(細い)
場所の点: 受け渡し口(11.15,13.3) 返却口(11.15,6.8) パントリー(11.15,10.05) レジ(12.35,0.5) トイレ(14.25,3.6) ドリンクバー(8.55,0.15) 入口(14.25,0.5) 待合(14.25,0.8)
人(普通の混み具合)
スタッフ 2 / 座っている客(椅子を引く仕掛け) 35
ロボット(見た目の確認用) 2
入口まわり(ドリンクバー・待合・レジ・入口)
外寸(建物だけ): 22.75 × 14.8 m
南の帯の奥行き: 1.6 m(椅子 0.45 + 卓 0.6 + ベンチ 0.55)
北のふち: 帯の足し算 14 m / ブースの列 14.8 m(ブース 8 × 1.85 m)→ 長いほうを採る
北の壁ぞいの卓の後ろ 0.8 m は壁でふさいだ(NorthRowBackFiller)
壁ぞいの2人席: 卓 0.7 × 0.6 m
上の列: 6 つ / 使える幅 7.95 m / すき間 0.75 m
下の列: 3 つ / 使える幅 4.35 m(ドリンクバーの 3.6 m を引いた残り)/ すき間 1.13 m
卓の数の決め方: いちばん狭い通路 0.8 m のときに入る数にそろえた(条件どうしを比べるため)
すき間と人の通れる幅: いちばん狭いすき間 0.75 m / 焼いた NavMesh の通行幅 0.5 m(navAgentRadius × 2)→ 壁側の席に入れる
ドリンクバー: カウンター 3.6 × 0.7 m を x 6.75..10.35 に置いた(下の列の東の残り 3.6 m)
奥行き: カウンター 0.7 + 立つ場所 1.5 + 並ぶ点の列 0.6 + 通路との隙間 0.1 = 2.9 m
くぼみ(アルコーブ): 南へ 1.3 m(3.6 m 幅ぶんだけ壁を外へ出した)
立つ点 4 つ(z 0.15)/ 並ぶ点 3 つ(z 1.2)
並ぶ点の北のふち 1.5 m < 南の細い通路の南のふち 1.6 m → 通路と重ならない
待合: x 12.75..15.75 / 奥行き 1.6 m → 4.8 m^2(レジの台と店員側は含まない)
必要な広さ: 8 人 × 0.5 m^2 = 4 m^2 → 足りている
レジ: 台 0.8 × 0.5 m(x 11.95..12.75)。台をはさんで南が客側・北が店員側
店員側(レジの内側): 0.8 × 1.2 m(x 11.95..12.75 / z 1.6..2.8)/ スタッフの立つ点 (12.35, 2.2)
入口は西(太い通路 x 11.95)だけ、幅 1.2 m。北は壁、東はトイレの西の壁、南はレジの台 → 客の列や待合の中は通らない
焼いた面で残る入口の幅の見込み: 人用 0.8 m / ロボット用 0.7 m(必要 0.7 m 以上)
客の並ぶ点 3 つ(レジ台の南, z 0.5)。店員側は NavMesh の区画を分けてあるので、客は入れない
入口: 中心 x 14.25 / 幅 2 m(太い通路の東。トイレは z 1.6..5.6 へ北寄せ)
ロボットの待機場所(B2)
太い通路: x 10.35..11.95(幅 1.6 m)/ ロボットの半径 0.25 m
待機01: (11.55, 11.7) / 西に残る通り道 0.95 m / 東のカウンター面まで 0.15 m / 受け渡し口の前の点まで 1.65 m・パントリー 1.7 m・返却口 4.92 m
待機02: (11.55, 10.9) / 西に残る通り道 0.95 m / 東のカウンター面まで 0.15 m / 受け渡し口の前の点まで 2.43 m・パントリー 0.94 m・返却口 4.12 m
点どうしのいちばん近い距離: 0.8 m → 重ならない(直径 0.5 m)
西に残る通り道のいちばん狭いところ: 0.95 m → 太い通路をふさがない(必要 0.55 m 以上)
スタッフが立つ点(受け渡し口・パントリー・返却口の前。どれも通路の真ん中)までのいちばん近い距離: 0.94 m → 重ならない
スタッフの待つ点: 2 つ
待つ01: (11.55, 14.35) / 太い通路の西のふちまで 1 m・東のふちまで 0.2 m
待つ02: (10.95, 14.35) / 太い通路の西のふちまで 0.4 m・東のふちまで 0.8 m
受け渡し口の窓口の北のふち z 13.9 より北 → ロボットの行き先(受け渡し口 13.3 ・パントリー・返却口・待機場所)はすべて南なので、通り道をふさがない
点どうしのいちばん近い距離: 0.6 m → 重ならない(人の直径 0.4 m)
待っている人の左右に残る幅のいちばん狭いところ: 0.2 m(太い通路 1.6 m のうち)
床の塗り分け(見た目だけ。NavMesh は変えない)
ロボットの体が入れる床(水色): 128.58 m2(180 枚)
人だけが入れる床(薄い黄色): 0 m2(0 枚)
作り方: ロボット用 NavMesh(半径 0.25 m)を 0.25 m 外へ広げた範囲/人用 NavMesh(半径 0.2 m)を 0.2 m 広げた範囲から前者を引いた範囲。マス 0.05 m
すき間が 0.5 m より狭いところは、ロボット用 NavMesh が無いので水色にならない
場所の名前(真上の画面に出す文字)
13 個(床の上の文字。真上の画面にも写る)/ 重なりを避けてずらした 0 / 場所の外に出して線でつないだ 2 / 名前どうしの重なり 0 個(重なりなし)
ロボットの経路が、ロボット用の面だけを通っているか
経路 35 本 / 経路の点 193 個を、ロボット用の面(半径 0.25 m)で確かめた
ロボット用の面に乗っていない点: 0 個 → ロボットはロボット用の面だけを通っている
レジの内側(店員側)に入れるか
店員側: x 11.95..12.75 / z 1.6..2.8(0.8 × 1.2 m)
スタッフが立つ点: (12.35, 2.2)
人用(半径 0.2 m): 寄せられた先 (12.35, 2.2) / 元からの距離 0 m / 店員側の中 / 受け渡し口から 11.38 m で行ける
歩ける幅: 東西 0 m / 南北 0 m / 入口(西のふち)0 m ← 入口が 0.7 m 未満。registerBackDepthZ を広げる
ロボット用(半径 0.25 m): 寄せられた先 (12.35, 2.2) / 元からの距離 0 m / 店員側の中 / 受け渡し口から 11.42 m で行ける
歩ける幅: 東西 0 m / 南北 0 m / 入口(西のふち)0 m ← 入口が 0.7 m 未満。registerBackDepthZ を広げる
細い通路の、焼いた NavMesh の歩ける幅
ロボット用(通行半径 0.25 m): 計算上は「通路の幅 − 0.5 m」
SouthAisle: 通路 1 m / 計算上 0.5 m / 焼いた面のいちばん狭いところ 0 m ← 0.5 m 足りない。左右にあるもの: 何もない / 何もない
NorthAisle: 通路 1 m / 計算上 0.5 m / 焼いた面のいちばん狭いところ 0 m ← 0.5 m 足りない。左右にあるもの: 何もない / 何もない
WestAisle_S: 通路 1 m / 計算上 0.5 m / 焼いた面のいちばん狭いところ 0 m ← 0.5 m 足りない。左右にあるもの: Floor03 1.4m(y=0.1) / 何もない
WestAisle_N: 通路 1 m / 計算上 0.5 m / 焼いた面のいちばん狭いところ 0 m ← 0.5 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle01_S: 通路 1 m / 計算上 0.5 m / 焼いた面のいちばん狭いところ 0 m ← 0.5 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle01_N: 通路 1 m / 計算上 0.5 m / 焼いた面のいちばん狭いところ 0 m ← 0.5 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle02_S: 通路 1 m / 計算上 0.5 m / 焼いた面のいちばん狭いところ 0 m ← 0.5 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle02_N: 通路 1 m / 計算上 0.5 m / 焼いた面のいちばん狭いところ 0 m ← 0.5 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle03_S: 通路 1 m / 計算上 0.5 m / 焼いた面のいちばん狭いところ 0 m ← 0.5 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle03_N: 通路 1 m / 計算上 0.5 m / 焼いた面のいちばん狭いところ 0 m ← 0.5 m 足りない。左右にあるもの: 何もない / 何もない
細い通路 10 本 → 10 本が計算上より狭い ← 直す必要あり
人用(通行半径 0.2 m): 計算上は「通路の幅 − 0.4 m」
SouthAisle: 通路 1 m / 計算上 0.6 m / 焼いた面のいちばん狭いところ 0 m ← 0.6 m 足りない。左右にあるもの: 何もない / 何もない
NorthAisle: 通路 1 m / 計算上 0.6 m / 焼いた面のいちばん狭いところ 0 m ← 0.6 m 足りない。左右にあるもの: 何もない / 何もない
WestAisle_S: 通路 1 m / 計算上 0.6 m / 焼いた面のいちばん狭いところ 0 m ← 0.6 m 足りない。左右にあるもの: Floor03 1.4m(y=0.1) / 何もない
WestAisle_N: 通路 1 m / 計算上 0.6 m / 焼いた面のいちばん狭いところ 0 m ← 0.6 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle01_S: 通路 1 m / 計算上 0.6 m / 焼いた面のいちばん狭いところ 0 m ← 0.6 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle01_N: 通路 1 m / 計算上 0.6 m / 焼いた面のいちばん狭いところ 0 m ← 0.6 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle02_S: 通路 1 m / 計算上 0.6 m / 焼いた面のいちばん狭いところ 0 m ← 0.6 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle02_N: 通路 1 m / 計算上 0.6 m / 焼いた面のいちばん狭いところ 0 m ← 0.6 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle03_S: 通路 1 m / 計算上 0.6 m / 焼いた面のいちばん狭いところ 0 m ← 0.6 m 足りない。左右にあるもの: 何もない / 何もない
ColumnAisle03_N: 通路 1 m / 計算上 0.6 m / 焼いた面のいちばん狭いところ 0 m ← 0.6 m 足りない。左右にあるもの: 何もない / 何もない
細い通路 10 本 → 10 本が計算上より狭い ← 直す必要あり
通れない場所に NavMesh が入っていないか
厨房(x 12.75..22.75 / z 5.6..14.8): 歩ける面 0 m2
事務所(x 15.75..22.75 / z 0..5.6): 歩ける面 0 m2
受け渡し口(x 11.95..12.75 / z 12.7..13.9): 歩ける面 0 m2
パントリー(x 11.95..12.75 / z 7.8..12.3): 歩ける面 0 m2
返却口(x 11.95..12.75 / z 6.3..7.3): 歩ける面 0 m2
ドリンクバー(x 6.75..10.35 / z -1.3..-0.6): 歩ける面 0 m2
レジ(x 11.95..12.75 / z 1.1..1.6): 歩ける面 0 m2
通れない場所 すべてに NavMesh は入っていない
席に入れるかの確かめ(通路 → 席の前)
人が立つ点 106 個(35 卓ぶん)を、通行半径 0.25 m で確かめた
すべての席に、通路から入れる
行き止まりの確かめ
ロボット用(半径 0.25 m)
ふさがないとき: 届かない卓 0 / 35
細い通路 10 本を1か所ずつふさいで確かめた → どれをふさいでも、ほかの全卓に届く(行き止まりなし)
人用(半径 0.2 m)
ふさがないとき: 届かない卓 0 / 35
細い通路 10 本を1か所ずつふさいで確かめた → どれをふさいでも、ほかの全卓に届く(行き止まりなし)
真上からの見通し(天井・屋根がないかの確認)
調べた点 36(受け渡し口+全卓の停止点)
最初に当たったもの: ServingRobot01 × 1
最初に当たったもの: Floor01 × 35
上を覆うもの: 1 点 → ServingRobot01 (y=1.2)
焼いた NavMesh のしらべ(NavMesh.CalculateTriangulation)
ロボット用(半径 0.25 m):三角形 246 / 歩ける面 79.07 m2
受け渡し口からつながる面: 79.07 m2
つながっていない面: 0 m2
いちばん高い歩ける面: y=0.03 m
area の内訳: area0=78.71m2 area3=0.36m2
人用(半径 0.2 m):三角形 243 / 歩ける面 88.45 m2
受け渡し口からつながる面: 88.45 m2
つながっていない面: 0 m2
いちばん高い歩ける面: y=0.03 m
area の内訳: area0=88m2 area3=0.45m2
=== StoreLayout_Family(ファミレス型(客席+厨房+パントリー))===
細い通路 0.8 m
外寸: 21.95 × 16.25 m(風除室を含む)
道のり(受け渡し口 → 卓): いちばん短い 2.12 m / いちばん長い 17.85 m / 平均 9.77 m
LOG351 の 6〜22 m に入る卓: 28 / 35
測れた卓 35 / 測れなかった卓 0
細い通路 1 m
外寸: 22.75 × 16.25 m(風除室を含む)
道のり(受け渡し口 → 卓): いちばん短い 1.84 m / いちばん長い 18 m / 平均 9.78 m
LOG351 の 6〜22 m に入る卓: 28 / 35
測れた卓 35 / 測れなかった卓 0
細い通路 1.2 m
外寸: 23.55 × 16.25 m(風除室を含む)
道のり(受け渡し口 → 卓): いちばん短い 1.59 m / いちばん長い 19.53 m / 平均 9.91 m
LOG351 の 6〜22 m に入る卓: 28 / 35
測れた卓 35 / 測れなかった卓 0
=== StoreLayout_Grid(格子型(比べる用))===
細い通路 0.8 m
外寸: 9.7 × 16.6 m(風除室を含む)
道のり(受け渡し口 → 卓): いちばん短い 5.37 m / いちばん長い 21.1 m / 平均 13.02 m
LOG351 の 6〜22 m に入る卓: 33 / 35
測れた卓 35 / 測れなかった卓 0
細い通路 1 m
外寸: 9.7 × 16.6 m(風除室を含む)
道のり(受け渡し口 → 卓): いちばん短い 5.27 m / いちばん長い 21 m / 平均 12.93 m
LOG351 の 6〜22 m に入る卓: 33 / 35
測れた卓 35 / 測れなかった卓 0
細い通路 1.2 m
外寸: 9.7 × 16.6 m(風除室を含む)
道のり(受け渡し口 → 卓): いちばん短い 5.19 m / いちばん長い 20.9 m / 平均 12.83 m
LOG351 の 6〜22 m に入る卓: 33 / 35
測れた卓 35 / 測れなかった卓 0
店の設定の書き出し(conditions.json・Build Store・2026-10-21 12:09)
{
"run_name": "family_v0",
"written_at": "2026-10-21 12:09:43",
"unity_version": "2022.3.62f3",
"trips_csv_columns": "condition_id,aisle_width_m,crowd,robots,seed,robot_id,trip_id,table_id,distance_m,total_s,stopped_s,n_stops,stop_durations_s,stuck,timeout,wait_aisle_s,max_decel_mps2,min_clearance_m,met_robot,table_ids,dishes,dish_ready_s,dish_arrived_s,undelivered",
"settings": {
"store_layout": {"aisleWidthOptions":[0.800000011920929,1.0,1.2000000476837159],"aisleWidthIndex":1,"deliveryOffsetFromAisleCenter":0.0,"aisleEntranceSetback":0.800000011920929,"aisleEntranceSideOffset":0.44999998807907107,"wallHeight":2.4000000953674318,"wallThickness":0.15000000596046449,"floorThickness":0.20000000298023225,"showAisleMarks":true,"aisleMarkThickness":0.019999999552965165,"showFloorPaint":true,"floorPaintCellSize":0.05000000074505806,"floorPaintHeight":0.029999999329447748,"showPlaceNames":true,"placeNameSize":0.14000000059604646,"placeNameHeight":0.05999999865889549,"navAgentRadius":0.25,"navAgentHeight":1.2000000476837159,"navAgentClimb":0.20000000298023225,"navAgentSlope":10.0,"navVoxelSize":0.05000000074505806,"navMinRegionArea":0.30000001192092898,"navBoundsPadding":1.0,"mainAisleWidth":1.600000023841858,"crossAisleWidth":1.2000000476837159,"kitchenDepthX":10.0,"serviceStripDepth":0.800000011920929,"toiletSize":{"x":3.0,"y":4.0},"toiletDoorWidthZ":1.0,"passFromNorthZ":1.5,"passWindowLengthZ":1.2000000476837159,"pantryFromNorthZ":2.5,"pantryLengthZ":4.5,"returnFromNorthZ":8.0,"returnWindowLengthZ":1.0,"wallBoothCount":8,"boothTableSize":{"x":1.399999976158142,"y":0.75},"benchDepth":0.550000011920929,"benchSeatHeight":0.44999998807907107,"benchBackThickness":0.10000000149011612,"benchHeight":1.100000023841858,"benchSeatGapX":0.05000000074505806,"benchSeatPadThickness":0.019999999552965165,"wallTableSize":{"x":0.699999988079071,"y":0.6000000238418579},"wallTableGapX":0.6000000238418579,"wallTableCountFromNarrowestAisle":true,"seatApproachGapZ":0.30000001192092898,"islandColumns":3,"islandTablesPerColumn":3,"islandTableSize":{"x":0.75,"y":1.2000000476837159},"islandTableGapZ":0.10000000149011612,"chairDepth":0.44999998807907107,"chairWidth":0.44999998807907107,"chairHeight":0.44999998807907107,"tableHeight":0.7200000286102295,"dividerHeight":1.100000023841858,"entranceWidthX":2.0,"vestibuleDepthZ":1.2000000476837159,"waitingDepthZ":0.0,"registerSize":{"x":0.800000011920929,"y":0.5},"registerQueueCount":3,"registerQueuePitch":0.5,"registerBackDepthZ":1.2000000476837159,"registerBackMinOpenZ":0.699999988079071,"drinkBarSize":{"x":3.5999999046325685,"y":0.699999988079071},"counterHeight":1.0,"approachGap":0.6000000238418579,"waitingStandCount":8,"waitingStandAreaM2":0.5,"robotStandbyCount":2,"robotStandbySideOffset":0.4000000059604645,"robotStandbyFromPassZ":1.600000023841858,"robotStandbyPitchZ":0.800000011920929,"robotStandbyMinClearX":0.550000011920929,"staffStandbyCount":2,"staffStandbyFromPassZ":0.44999998807907107,"staffStandbyPitchX":0.6000000238418579,"staffStandbySideOffset":0.4000000059604645,"drinkBarStandCount":4,"drinkBarStandDepthZ":1.5,"drinkBarQueueCount":3,"drinkBarQueuePitch":0.5,"drinkBarQueueDepthZ":0.6000000238418579,"drinkBarAisleClearZ":0.10000000149011612},
"robot_spec": {"diameter":0.5,"height":1.2000000476837159,"maxSpeed":0.6000000238418579,"acceleration":1.0,"deceleration":1.0,"turnSpeedDegPerSec":180.0,"sensorRange":0.6000000238418579,"sensorWidth":0.5,"sensorHeight":1.2000000476837159,"resumeAfterClearSec":1.0,"replanAfterStoppedSec":10.0,"stuckAfterStoppedSec":60.0,"stuckRecoveryWaitSec":15.0,"maxTripSec":600.0,"aisleYielding":true,"tableDwellSec":25.0,"arriveRadius":0.25,"cornerReachDistance":0.11999999731779099,"stoppedSpeedEpsilon":0.019999999552965165,"clearanceProbeHeight":0.30000001192092898,"clearanceProbeRange":3.0,"pathBlockSearchRadius":2.0,"avoidancePriority":0,"repathRetrySec":1.0,"noPathForceReplanSec":10.0,"escapeToAisleEntranceOnPartialPath":true},
"people_spec": {"personRadius":0.20000000298023225,"personHeight":1.7000000476837159,"staffCount":2,"staffSpeed":1.0,"staffTaskWeights":[1.0,1.0,1.0,1.0,1.0],"staffDwellAtPassSec":3.0,"staffDwellAtTableSec":5.0,"staffDwellAtReturnSec":5.0,"staffDwellAtPantrySec":5.0,"staffDwellAtWaitingSec":5.0,"staffDwellAtRegisterSec":30.0,"guestCountByCrowd":[1,4,8],"guestSpeed":0.800000011920929,"guestTripWeights":[1.0,1.0,1.0],"guestDwellAtPoiSec":10.0,"guestDwellAtRegisterSec":20.0,"guestDwellAtTableSec":30.0,"warmupOccupancy":0.800000011920929,"jamNarrowAfterSec":1.0,"jamNarrowRadius":0.11999999731779099,"jamBackOffAfterSec":3.0,"jamStuckSpeed":0.05000000074505806,"waitingCountByCrowd":[0,2,5],"waitingShuffleSec":25.0,"chairPullHoldSec":30.0,"chairPullDistance":0.30000001192092898,"chairPullIdleMeanIntervalSec":1800.0,"guestGiveUpSec":60.0,"guestWarpHomeSec":60.0,"staffGiveUpSec":60.0,"staffGiveUpMaxTries":3,"registerBackArea":3,"avoidancePriority":50,"avoidanceQuality":3,"acceleration":2.0,"angularSpeedDegPerSec":360.0,"stoppingDistance":0.30000001192092898,"robotYieldEnabled":true,"robotAisleLookAheadM":3.0,"robotAisleMarginM":0.30000001192092898,"robotDetourExtraMaxM":5.0,"robotFaceToFaceM":1.5,"robotKeepClearM":0.30000001192092898,"robotSideStepM":0.5,"robotBackOffSearchM":4.0,"robotPassedClearM":0.5,"robotYieldMaxSec":20.0,"robotCarelessChance":0.019999999552965165,"robotContactGapM":0.10000000149011612,"robotNudgeSpeedMps":0.5},
"experiment_plan": {"runName":"family_v0","aisleWidthIndices":[0,1,2],"crowdLevels":[0,1,2],"robotCounts":[1,2],"tripsPerCondition":100,"seeds":[0,1,2,3,4],"timeScale":1.0,"kioxiaVolumePath":"/Volumes/KIOXIA","kioxiaOutputRoot":"/Volumes/KIOXIA/r2r2r-data/service_sim/unity","trialAisleWidthIndex":1,"trialCrowd":1,"trialRobots":2,"trialSeed":0,"trialTrips":20,"trialTimeScaleFast":10.0,"trialCaptureDeltaTime":0.019999999552965165,"trialMaxWallSeconds":2400.0,"trialOutputRoot":"/Users/shimazueiichi/ServiceSim_tmp","measureMinutes":120.0,"warmupMinutes":15.0,"measureCaptureDeltaTime":0.05000000074505806,"referenceCaptureDeltaTime":0.019999999552965165,"disableRenderingWhileRunning":false,"showLabelsWhileRunning":false,"stage3OutputRoot":"/Users/shimazueiichi/ServiceSim_tmp/stage3","stage3Resume":true,"screenshotDir":"/Users/shimazueiichi/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/unity/screens","screenshotWidth":1920,"screenshotHeight":1080}
},
"derived": {
"store_kind": "ファミレス型(客席+厨房+パントリー)",
"aisle_width_m": 1,
"floor_min_x": 0,
"floor_max_x": 22.75,
"floor_min_z": -1.45,
"floor_max_z": 14.8,
"outer_size_x_m": 22.75,
"outer_size_z_m": 16.25,
"table_count": 35,
"aisle_count": 12,
"narrow_aisle_count": 10
},
"conditions": [
{"condition_id": "w080_empty_r1", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "empty", "walkers": 3, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w080_empty_r2", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "empty", "walkers": 3, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w080_normal_r1", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "normal", "walkers": 6, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w080_normal_r2", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "normal", "walkers": 6, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w080_busy_r1", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "busy", "walkers": 10, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w080_busy_r2", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "busy", "walkers": 10, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_empty_r1", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "empty", "walkers": 3, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_empty_r2", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "empty", "walkers": 3, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_normal_r1", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "normal", "walkers": 6, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_normal_r2", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "normal", "walkers": 6, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_busy_r1", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "busy", "walkers": 10, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_busy_r2", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "busy", "walkers": 10, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_empty_r1", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "empty", "walkers": 3, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_empty_r2", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "empty", "walkers": 3, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_normal_r1", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "normal", "walkers": 6, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_normal_r2", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "normal", "walkers": 6, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_busy_r1", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "busy", "walkers": 10, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_busy_r2", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "busy", "walkers": 10, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true}
]
}
受け渡し口から各卓までの道のり(table_path_lengths_family.csv)
table_id,seat_kind,aisle,table_x,table_z,delivery_x,delivery_z,path_m_w080,path_m_w100,path_m_w120,status_w080,status_w100,status_w120
1,booth,WestAisle_S,0.7,0.93,2,0.93,17.85,18,19.53,ok,ok,ok
2,booth,WestAisle_S,0.7,2.78,2,2.78,16.79,17.24,17.71,ok,ok,ok
3,booth,WestAisle_S,0.7,4.63,2,4.63,14.94,15.4,15.87,ok,ok,ok
4,booth,WestAisle_S,0.7,6.48,2,6.48,13.41,13.79,14.22,ok,ok,ok
5,booth,WestAisle_N,0.7,8.33,2,8.33,12.31,12.82,13.33,ok,ok,ok
6,booth,WestAisle_N,0.7,10.18,2,10.18,10.47,10.99,11.5,ok,ok,ok
7,booth,WestAisle_N,0.7,12.03,2,12.03,9.56,9.88,10.28,ok,ok,ok
8,booth,WestAisle_N,0.7,13.88,2,13.88,11.26,11.23,11.17,ok,ok,ok
9,wall,NorthAisle,2.95,13.55,2.95,12.2,8.73,9.04,9.33,ok,ok,ok
10,wall,NorthAisle,4.52,13.55,4.52,12.2,7.4,7.59,7.76,ok,ok,ok
11,wall,NorthAisle,6.09,13.55,6.09,12.2,6.07,6.14,6.19,ok,ok,ok
12,wall,NorthAisle,7.66,13.55,7.66,12.2,4.74,4.69,4.62,ok,ok,ok
13,wall,NorthAisle,9.23,13.55,9.23,12.2,3.42,3.25,3.06,ok,ok,ok
14,wall,NorthAisle,10.8,13.55,10.8,12.2,2.12,1.84,1.59,ok,ok,ok
15,wall,SouthAisle,2.95,0.85,2.95,2.2,16.44,16.52,16.64,ok,ok,ok
16,wall,SouthAisle,5.08,0.85,5.08,2.2,16.03,14.82,15.91,ok,ok,ok
17,wall,SouthAisle,7.2,0.85,7.2,2.2,14.5,14.15,13.8,ok,ok,ok
18,island,ColumnAisle01_S,3.43,3.4,4.85,3.4,14.14,14.19,14.27,ok,ok,ok
19,island,ColumnAisle01_S,3.43,4.7,4.85,4.7,12.84,12.9,12.98,ok,ok,ok
20,island,ColumnAisle01_S,3.43,6,4.85,6,11.55,11.63,11.74,ok,ok,ok
21,island,ColumnAisle02_S,6.28,3.4,7.7,3.4,11.79,11.68,11.55,ok,ok,ok
22,island,ColumnAisle02_S,6.28,4.7,7.7,4.7,10.49,10.39,10.26,ok,ok,ok
23,island,ColumnAisle02_S,6.28,6,7.7,6,9.2,9.11,9.01,ok,ok,ok
24,island,ColumnAisle03_S,9.13,3.4,10.55,3.4,10.37,10.18,10,ok,ok,ok
25,island,ColumnAisle03_S,9.13,4.7,10.55,4.7,9.08,8.9,8.71,ok,ok,ok
26,island,ColumnAisle03_S,9.13,6,10.55,6,7.79,7.61,7.43,ok,ok,ok
27,island,ColumnAisle01_N,3.43,8.4,4.85,8.4,10.19,10.3,10.43,ok,ok,ok
28,island,ColumnAisle01_N,3.43,9.7,4.85,9.7,8.89,9.01,9.14,ok,ok,ok
29,island,ColumnAisle01_N,3.43,11,4.85,11,7.6,7.74,7.9,ok,ok,ok
30,island,ColumnAisle02_N,6.28,8.4,7.7,8.4,7.75,7.72,7.65,ok,ok,ok
31,island,ColumnAisle02_N,6.28,9.7,7.7,9.7,6.45,6.42,6.36,ok,ok,ok
32,island,ColumnAisle02_N,6.28,11,7.7,11,5.17,5.15,5.11,ok,ok,ok
33,island,ColumnAisle03_N,9.13,8.4,10.55,8.4,5.43,5.26,5.1,ok,ok,ok
34,island,ColumnAisle03_N,9.13,9.7,10.55,9.7,4.18,4.02,3.86,ok,ok,ok
35,island,ColumnAisle03_N,9.13,11,10.55,11,2.95,2.82,2.69,ok,ok,ok
段階2:3通りの回し方の summary.json と run_info.json
等倍(stage2_family_v0_ts1)
{
"run_name": "family_v0",
"written_at": "2026-10-21 06:53:10",
"note": "robot_speed_mps = distance_m / (total_s - stopped_s)。stopped_s には、動けなくなった60秒+15秒と、通路の入口で待った秒数も入る。pause_s は stop_durations_s(自分で動き出した止まりだけ)の分布。",
"stop_causes": [
{"cause": "スタッフとすれ違った", "count": 6, "total_s": 73.9078, "mean_s": 12.318, "longest_s": 60.0026, "path_blocked_count": 0},
{"cause": "椅子・荷物・ベビーカー", "count": 17, "total_s": 838.9169, "mean_s": 49.3481, "longest_s": 60.0035, "path_blocked_count": 0},
{"cause": "分からない", "count": 35, "total_s": 27.6412, "mean_s": 0.7897, "longest_s": 2.1291, "path_blocked_count": 0},
{"cause": "人が立ち止まっていた", "count": 7, "total_s": 147.4799, "mean_s": 21.0686, "longest_s": 60.0006, "path_blocked_count": 0},
{"cause": "通路の順番待ち", "count": 3, "total_s": 182.7856, "mean_s": 60.9285, "longest_s": 71.2395, "path_blocked_count": 0},
{"cause": "もう1台のロボット", "count": 2, "total_s": 3.6894, "mean_s": 1.8447, "longest_s": 1.8832, "path_blocked_count": 0},
{"cause": "お客さんとすれ違った", "count": 1, "total_s": 60.0006, "mean_s": 60.0006, "longest_s": 60.0006, "path_blocked_count": 0}
],
"conditions": [
{"condition_id": "w100_normal_r2", "aisle_width_m": 1, "crowd": "normal", "robots": 2, "trips": 20, "robot_speed_mps_median": 0.5502, "stopped_s_median": 77.6128, "stopped_s_p90": 109.0033, "pause_s_median": 1.0184, "pause_s_p90": 9.3549, "pause_count": 52, "wait_aisle_s_median": 0, "stuck_rate": 0.8, "stuck_count": 16, "timeout_rate": 0, "timeout_count": 0, "pause_rate": 1, "met_robot_rate": 0.15}
]
}
{
"run_label": "ts1",
"written_at": "2026-10-21 06:53:10",
"condition_id": "w100_normal_r2",
"store_kind": "StoreLayout_Family",
"time_mode": "等倍(timeScale 1)",
"time_scale": 1,
"capture_delta_time": 0,
"aisle_width_m": 1,
"crowd": "normal",
"robots": 2,
"seed": 0,
"trips_requested": 20,
"trips_done": 20,
"stopped_early": false,
"sim_seconds": 1055.758,
"wall_seconds": 1057.046,
"speedup": 0.9988,
"frame_delta_s": {"count": 530363, "mean": 0.002, "min": 0.0012, "p50": 0.0017, "p90": 0.0025, "p99": 0.0066, "max": 0.3333},
"robot_step_m": {"count": 530363, "mean": 0.0003, "min": 0, "p50": 0, "p90": 0.001, "p99": 0.0031, "max": 0.1947},
"frames_over_005m": 5,
"frames_over_005m_ratio": 0
}
timeScale 10(stage2_family_v0_ts10)
{
"run_name": "family_v0",
"written_at": "2026-10-21 07:08:56",
"note": "robot_speed_mps = distance_m / (total_s - stopped_s)。stopped_s には、動けなくなった60秒+15秒と、通路の入口で待った秒数も入る。pause_s は stop_durations_s(自分で動き出した止まりだけ)の分布。",
"stop_causes": [
{"cause": "分からない", "count": 31, "total_s": 86.8357, "mean_s": 2.8012, "longest_s": 60.0139, "path_blocked_count": 0},
{"cause": "人が立ち止まっていた", "count": 14, "total_s": 302.8371, "mean_s": 21.6312, "longest_s": 60.0589, "path_blocked_count": 0},
{"cause": "椅子・荷物・ベビーカー", "count": 13, "total_s": 535.9957, "mean_s": 41.2304, "longest_s": 60.0601, "path_blocked_count": 0},
{"cause": "スタッフとすれ違った", "count": 2, "total_s": 7.279, "mean_s": 3.6395, "longest_s": 5.027, "path_blocked_count": 0},
{"cause": "お客さんとすれ違った", "count": 4, "total_s": 123.3035, "mean_s": 30.8259, "longest_s": 60.0179, "path_blocked_count": 0},
{"cause": "通路の順番待ち", "count": 1, "total_s": 58.1321, "mean_s": 58.1321, "longest_s": 58.1321, "path_blocked_count": 0},
{"cause": "もう1台のロボット", "count": 3, "total_s": 180.0339, "mean_s": 60.0113, "longest_s": 60.0147, "path_blocked_count": 0}
],
"conditions": [
{"condition_id": "w100_normal_r2", "aisle_width_m": 1, "crowd": "normal", "robots": 2, "trips": 20, "robot_speed_mps_median": 0.5351, "stopped_s_median": 77.157, "stopped_s_p90": 91.443, "pause_s_median": 1.0158, "pause_s_p90": 12.307, "pause_count": 50, "wait_aisle_s_median": 0, "stuck_rate": 0.85, "stuck_count": 17, "timeout_rate": 0, "timeout_count": 0, "pause_rate": 0.8, "met_robot_rate": 0.35}
]
}
{
"run_label": "ts10",
"written_at": "2026-10-21 07:08:56",
"condition_id": "w100_normal_r2",
"store_kind": "StoreLayout_Family",
"time_mode": "timeScale 10",
"time_scale": 10,
"capture_delta_time": 0,
"aisle_width_m": 1,
"crowd": "normal",
"robots": 2,
"seed": 0,
"trips_requested": 20,
"trips_done": 20,
"stopped_early": false,
"sim_seconds": 1061.47,
"wall_seconds": 106.4115,
"speedup": 9.9751,
"frame_delta_s": {"count": 45163, "mean": 0.0235, "min": 0.0124, "p50": 0.0189, "p90": 0.0298, "p99": 0.067, "max": 2.8226},
"robot_step_m": {"count": 45163, "mean": 0.0034, "min": 0, "p50": 0, "p90": 0.0116, "p99": 0.0372, "max": 0.3296},
"frames_over_005m": 49,
"frames_over_005m_ratio": 0.0011
}
1コマ固定 0.02秒(stage2_family_v0_fixed)
{
"run_name": "family_v0",
"written_at": "2026-10-21 07:00:59",
"note": "robot_speed_mps = distance_m / (total_s - stopped_s)。stopped_s には、動けなくなった60秒+15秒と、通路の入口で待った秒数も入る。pause_s は stop_durations_s(自分で動き出した止まりだけ)の分布。",
"stop_causes": [
{"cause": "スタッフとすれ違った", "count": 5, "total_s": 12.68, "mean_s": 2.536, "longest_s": 4.56, "path_blocked_count": 0},
{"cause": "椅子・荷物・ベビーカー", "count": 17, "total_s": 732.814, "mean_s": 43.1067, "longest_s": 60.0012, "path_blocked_count": 0},
{"cause": "分からない", "count": 36, "total_s": 37.46, "mean_s": 1.0406, "longest_s": 4.26, "path_blocked_count": 0},
{"cause": "人が立ち止まっていた", "count": 9, "total_s": 251.8842, "mean_s": 27.9871, "longest_s": 60.0012, "path_blocked_count": 0},
{"cause": "通路の順番待ち", "count": 2, "total_s": 133.2993, "mean_s": 66.6496, "longest_s": 81.9383, "path_blocked_count": 0},
{"cause": "もう1台のロボット", "count": 3, "total_s": 121.6424, "mean_s": 40.5475, "longest_s": 60.0012, "path_blocked_count": 0},
{"cause": "お客さんとすれ違った", "count": 3, "total_s": 4.88, "mean_s": 1.6267, "longest_s": 1.94, "path_blocked_count": 0}
],
"conditions": [
{"condition_id": "w100_normal_r2", "aisle_width_m": 1, "crowd": "normal", "robots": 2, "trips": 20, "robot_speed_mps_median": 0.5458, "stopped_s_median": 79.9286, "stopped_s_p90": 107.616, "pause_s_median": 1.02, "pause_s_p90": 15.5102, "pause_count": 58, "wait_aisle_s_median": 0, "stuck_rate": 0.75, "stuck_count": 15, "timeout_rate": 0, "timeout_count": 0, "pause_rate": 1, "met_robot_rate": 0.4}
]
}
{
"run_label": "fixed",
"written_at": "2026-10-21 07:00:59",
"condition_id": "w100_normal_r2",
"store_kind": "StoreLayout_Family",
"time_mode": "1コマ固定 0.02 秒(captureDeltaTime)",
"time_scale": 1,
"capture_delta_time": 0.02,
"aisle_width_m": 1,
"crowd": "normal",
"robots": 2,
"seed": 0,
"trips_requested": 20,
"trips_done": 20,
"stopped_early": false,
"sim_seconds": 1071.723,
"wall_seconds": 109.972,
"speedup": 9.7454,
"frame_delta_s": {"count": 53565, "mean": 0.02, "min": 0.02, "p50": 0.02, "p90": 0.02, "p99": 0.02, "max": 0.02},
"robot_step_m": {"count": 53565, "mean": 0.0033, "min": 0, "p50": 0, "p90": 0.012, "p99": 0.012, "max": 0.012},
"frames_over_005m": 0,
"frames_over_005m_ratio": 0
}
1回目(11時3分の回・細い通路 1.0 m・普通・ロボット2台・種0・止まる点 0)の出力
出力先は Mac の ~/ServiceSim_tmp/one_family_v0_v3_w1_normal_r2_s0_off000/ で、KIOXIA の r2r2r-data/service_sim/unity/20261021_one_family_v0_v3_off000_before_fixes/run/ にコピーし、sha256 とバイト数が10ファイルとも一致することを確かめています。
summary.json
{
"run_name": "family_v0",
"written_at": "2026-10-21 11:11:28",
"note": "robot_speed_mps = distance_m / (total_s - stopped_s)。stopped_s には、動けなくなった60秒+15秒と、通路の入口で待った秒数も入る。pause_s は stop_durations_s(自分で動き出した止まりだけ)の分布。",
"sim_like": {"dish_wait_mean": 12.6789, "dish_wait_p90": 20.1984, "cold_share": 0.9636, "seat_wait_mean": 16.0748, "turn_mean": 1.8197, "robot_share": 0.1636, "robot_dishes": 18, "staff_dishes": 92, "requeued_dishes": 268, "groups": 44, "people_jams": 1028, "robot_blocked_jams": 726, "robot_blocked_jam_share": 0.7062, "chair_pulls": 497, "robot_contacts": 205, "robot_yield_waits": 292, "robot_yield_wait_s": 3790.773, "robot_yield_detours": 225, "register_crowd_max": 16, "register_crowd_mean": 10.4649, "drink_bar_queued": 16, "drink_bar_gave_up": 5, "drink_bar_wait_mean": 0.5336, "drink_bar_wait_p90": 0.9936, "gave_up": 79, "warped": 40, "table_freed": 44, "people_jam_total_s": 1111.543, "walkers_mean": 15.8773, "measured_minutes": 120},
"staff_time_s": {"待機": 84.1905, "料理": 8651.058, "呼び出し": 1556.593, "案内": 4688.497, "会計": 1227.822},
"dish_wait_parts": {"robot": {"dishes": 18, "pass_wait_mean_min": 1.4582, "carry_mean_min": 2.6842, "requeue_lost_mean_min": 1.433, "total_mean_min": 5.5754, "requeued_per_dish": 0.7778, "requeued_total": 14}, "staff": {"dishes": 92, "pass_wait_mean_min": 13.0016, "carry_mean_min": 0.6876, "requeue_lost_mean_min": 0.3796, "total_mean_min": 14.0688, "requeued_per_dish": 0, "requeued_total": 0}, "all": {"dishes": 110, "pass_wait_mean_min": 11.1127, "carry_mean_min": 1.0143, "requeue_lost_mean_min": 0.552, "total_mean_min": 12.6789, "requeued_per_dish": 0.1273, "requeued_total": 14}},
"job_wait_mean_s": {"料理": 807.1234, "呼び出し": 639.0439, "案内": 711.9146, "会計": 275.8537},
"staff_time_share": {"待機": 0.0052, "料理": 0.5337, "呼び出し": 0.096, "案内": 0.2893, "会計": 0.0758},
"stops": {"count": 313, "stuck_count": 76, "partial_path_count": 0},
"stop_causes": [
{"cause": "分からない", "count": 73, "total_s": 169.9491, "mean_s": 2.3281, "longest_s": 45.2496, "path_blocked_count": 0, "stuck_count": 0, "partial_path_count": 0, "no_path_total_s": 0},
{"cause": "椅子・荷物・ベビーカー", "count": 80, "total_s": 4185.705, "mean_s": 52.3213, "longest_s": 60.0494, "path_blocked_count": 0, "stuck_count": 63, "partial_path_count": 0, "no_path_total_s": 0},
{"cause": "スタッフとすれ違った", "count": 23, "total_s": 60.8499, "mean_s": 2.6456, "longest_s": 24.1499, "path_blocked_count": 0, "stuck_count": 0, "partial_path_count": 0, "no_path_total_s": 0},
{"cause": "人が立ち止まっていた", "count": 23, "total_s": 582.8959, "mean_s": 25.3433, "longest_s": 60.0494, "path_blocked_count": 0, "stuck_count": 4, "partial_path_count": 0, "no_path_total_s": 0},
{"cause": "お客さんとすれ違った", "count": 4, "total_s": 33.05, "mean_s": 8.2625, "longest_s": 19.9, "path_blocked_count": 0, "stuck_count": 0, "partial_path_count": 0, "no_path_total_s": 0},
{"cause": "通路の順番待ち", "count": 75, "total_s": 5855.598, "mean_s": 78.0746, "longest_s": 161.7553, "path_blocked_count": 0, "stuck_count": 0, "partial_path_count": 0, "no_path_total_s": 0},
{"cause": "もう1台のロボット", "count": 35, "total_s": 999.3433, "mean_s": 28.5527, "longest_s": 60.0494, "path_blocked_count": 0, "stuck_count": 9, "partial_path_count": 0, "no_path_total_s": 0}
],
"conditions": [
{"condition_id": "w100_normal_r2", "aisle_width_m": 1, "crowd": "normal", "robots": 2, "trips": 86, "robot_speed_mps_median": 0.5421, "stopped_s_median": 77.7251, "stopped_s_p90": 481.8305, "pause_s_median": 1.3, "pause_s_p90": 30.6198, "pause_count": 164, "wait_aisle_s_median": 0, "stuck_rate": 0.8837, "stuck_count": 76, "timeout_rate": 0.1047, "timeout_count": 9, "pause_rate": 0.9535, "met_robot_rate": 0.3372}
]
}
run_info.json
{
"run_label": "v3",
"written_at": "2026-10-21 11:11:28",
"condition_id": "w100_normal_r2",
"store_kind": "StoreLayout_Family",
"time_mode": "FixedStep",
"time_scale": 1,
"capture_delta_time": 0.05,
"aisle_width_m": 1,
"delivery_offset_m": 0,
"delivery_offset_used_m": 0,
"crowd": "normal",
"robots": 2,
"seed": 0,
"trips_requested": 0,
"trips_done": 86,
"stopped_early": false,
"sim_seconds": 8100.039,
"wall_seconds": 480.7841,
"speedup": 16.8476,
"frame_delta_s": {"count": 162259, "mean": 0.0499, "min": 0.02, "p50": 0.05, "p90": 0.05, "p99": 0.05, "max": 0.05},
"robot_step_m": {"count": 162259, "mean": 0.0047, "min": 0, "p50": 0, "p90": 0.03, "p99": 0.03, "max": 0.03},
"people_step_m": {"count": 162259, "mean": 0.0488, "min": 0, "p50": 0.04, "p90": 0.05, "p99": 0.05, "max": 18.3853},
"frames_over_005m": 0,
"frames_over_005m_ratio": 0,
"people_frames_over_005m": 29304,
"people_frames_over_005m_ratio": 0.1806,
"chair_pulls": 497,
"chair_pulls_by_reason": {"ドリンクバー": 135, "食事中": 130, "戻って座る": 139, "座る": 40, "トイレ": 10, "会計": 35, "退店": 8}
}
conditions.json
{
"run_name": "family_v0",
"written_at": "2026-10-21 11:11:28",
"unity_version": "2022.3.62f3",
"trips_csv_columns": "condition_id,aisle_width_m,crowd,robots,seed,robot_id,trip_id,table_id,distance_m,total_s,stopped_s,n_stops,stop_durations_s,stuck,timeout,wait_aisle_s,max_decel_mps2,min_clearance_m,met_robot,table_ids,dishes,dish_ready_s,dish_arrived_s,undelivered",
"settings": {
"store_layout": {"aisleWidthOptions":[0.800000011920929,1.0,1.2000000476837159],"aisleWidthIndex":1,"deliveryOffsetFromAisleCenter":0.0,"aisleEntranceSetback":0.800000011920929,"aisleEntranceSideOffset":0.44999998807907107,"wallHeight":2.4000000953674318,"wallThickness":0.15000000596046449,"floorThickness":0.20000000298023225,"showAisleMarks":true,"aisleMarkThickness":0.019999999552965165,"navAgentRadius":0.25,"navAgentHeight":1.2000000476837159,"navAgentClimb":0.20000000298023225,"navAgentSlope":10.0,"navVoxelSize":0.05000000074505806,"navMinRegionArea":0.30000001192092898,"navBoundsPadding":1.0,"mainAisleWidth":1.600000023841858,"crossAisleWidth":1.2000000476837159,"kitchenDepthX":10.0,"serviceStripDepth":0.800000011920929,"toiletSize":{"x":3.0,"y":4.0},"toiletDoorWidthZ":1.0,"passFromNorthZ":1.5,"passWindowLengthZ":1.2000000476837159,"pantryFromNorthZ":2.5,"pantryLengthZ":4.5,"returnFromNorthZ":8.0,"returnWindowLengthZ":1.0,"wallBoothCount":8,"boothTableSize":{"x":1.399999976158142,"y":0.75},"benchDepth":0.550000011920929,"benchSeatHeight":0.44999998807907107,"benchBackThickness":0.10000000149011612,"benchHeight":1.100000023841858,"benchSeatGapX":0.05000000074505806,"benchSeatPadThickness":0.019999999552965165,"wallTableSize":{"x":0.699999988079071,"y":0.6000000238418579},"wallTableGapX":0.6000000238418579,"wallTableCountFromNarrowestAisle":true,"seatApproachGapZ":0.30000001192092898,"islandColumns":3,"islandTablesPerColumn":3,"islandTableSize":{"x":0.75,"y":1.2000000476837159},"islandTableGapZ":0.10000000149011612,"chairDepth":0.44999998807907107,"chairWidth":0.44999998807907107,"chairHeight":0.44999998807907107,"tableHeight":0.7200000286102295,"dividerHeight":1.100000023841858,"entranceWidthX":2.0,"vestibuleDepthZ":1.2000000476837159,"waitingDepthZ":0.0,"registerSize":{"x":1.0,"y":0.5},"registerQueueCount":3,"registerQueuePitch":0.5,"drinkBarSize":{"x":3.5999999046325685,"y":0.699999988079071},"counterHeight":1.0,"approachGap":0.6000000238418579,"waitingStandCount":8,"waitingStandAreaM2":0.5,"robotStandbyCount":2,"robotStandbySideOffset":0.4000000059604645,"robotStandbyFromPassZ":1.600000023841858,"robotStandbyPitchZ":0.800000011920929,"robotStandbyMinClearX":0.550000011920929,"staffStandbyCount":2,"staffStandbyFromPassZ":0.44999998807907107,"staffStandbyPitchX":0.6000000238418579,"staffStandbySideOffset":0.4000000059604645,"drinkBarStandCount":4,"drinkBarStandDepthZ":1.5,"drinkBarQueueCount":3,"drinkBarQueuePitch":0.5,"drinkBarQueueDepthZ":0.6000000238418579,"drinkBarAisleClearZ":0.10000000149011612},
"robot_spec": {"diameter":0.5,"height":1.2000000476837159,"maxSpeed":0.6000000238418579,"acceleration":1.0,"deceleration":1.0,"turnSpeedDegPerSec":180.0,"sensorRange":0.6000000238418579,"sensorWidth":0.5,"sensorHeight":1.2000000476837159,"resumeAfterClearSec":1.0,"replanAfterStoppedSec":10.0,"stuckAfterStoppedSec":60.0,"stuckRecoveryWaitSec":15.0,"maxTripSec":600.0,"aisleYielding":true,"tableDwellSec":25.0,"arriveRadius":0.25,"cornerReachDistance":0.11999999731779099,"stoppedSpeedEpsilon":0.019999999552965165,"clearanceProbeHeight":0.30000001192092898,"clearanceProbeRange":3.0,"pathBlockSearchRadius":2.0,"avoidancePriority":0,"repathRetrySec":1.0,"noPathForceReplanSec":10.0,"escapeToAisleEntranceOnPartialPath":true},
"people_spec": {"personRadius":0.20000000298023225,"personHeight":1.7000000476837159,"staffCount":2,"staffSpeed":1.0,"staffTaskWeights":[1.0,1.0,1.0,1.0,1.0],"staffDwellAtPassSec":3.0,"staffDwellAtTableSec":5.0,"staffDwellAtReturnSec":5.0,"staffDwellAtPantrySec":5.0,"staffDwellAtWaitingSec":5.0,"staffDwellAtRegisterSec":30.0,"guestCountByCrowd":[1,4,8],"guestSpeed":0.800000011920929,"guestTripWeights":[1.0,1.0,1.0],"guestDwellAtPoiSec":10.0,"guestDwellAtRegisterSec":20.0,"guestDwellAtTableSec":30.0,"warmupOccupancy":0.800000011920929,"jamNarrowAfterSec":1.0,"jamNarrowRadius":0.11999999731779099,"jamBackOffAfterSec":3.0,"jamStuckSpeed":0.05000000074505806,"waitingCountByCrowd":[0,2,5],"waitingShuffleSec":25.0,"chairPullHoldSec":30.0,"chairPullDistance":0.30000001192092898,"chairPullIdleMeanIntervalSec":1800.0,"guestGiveUpSec":60.0,"guestWarpHomeSec":60.0,"staffGiveUpSec":60.0,"staffGiveUpMaxTries":3,"avoidancePriority":50,"avoidanceQuality":3,"acceleration":2.0,"angularSpeedDegPerSec":360.0,"stoppingDistance":0.30000001192092898,"robotYieldEnabled":true,"robotAisleLookAheadM":3.0,"robotAisleMarginM":0.30000001192092898,"robotDetourExtraMaxM":5.0,"robotFaceToFaceM":1.5,"robotKeepClearM":0.30000001192092898,"robotSideStepM":0.5,"robotBackOffSearchM":4.0,"robotPassedClearM":0.5,"robotYieldMaxSec":20.0,"robotCarelessChance":0.019999999552965165,"robotContactGapM":0.10000000149011612,"robotNudgeSpeedMps":0.5},
"experiment_plan": {"runName":"family_v0","aisleWidthIndices":[0,1,2],"crowdLevels":[0,1,2],"robotCounts":[1,2],"tripsPerCondition":100,"seeds":[0,1,2,3,4],"timeScale":1.0,"kioxiaVolumePath":"/Volumes/KIOXIA","kioxiaOutputRoot":"/Volumes/KIOXIA/r2r2r-data/service_sim/unity","trialAisleWidthIndex":1,"trialCrowd":1,"trialRobots":2,"trialSeed":0,"trialTrips":20,"trialTimeScaleFast":10.0,"trialCaptureDeltaTime":0.019999999552965165,"trialMaxWallSeconds":2400.0,"trialOutputRoot":"/Users/shimazueiichi/ServiceSim_tmp","measureMinutes":120.0,"warmupMinutes":15.0,"measureCaptureDeltaTime":0.05000000074505806,"referenceCaptureDeltaTime":0.019999999552965165,"disableRenderingWhileRunning":false,"showLabelsWhileRunning":false,"stage3OutputRoot":"/Users/shimazueiichi/ServiceSim_tmp/stage3","stage3Resume":true,"screenshotDir":"/Users/shimazueiichi/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/unity/screens","screenshotWidth":1920,"screenshotHeight":1080}
},
"derived": {
"store_kind": "ファミレス型(客席+厨房+パントリー)",
"aisle_width_m": 1,
"floor_min_x": 0,
"floor_max_x": 22.75,
"floor_min_z": -1.45,
"floor_max_z": 14.8,
"outer_size_x_m": 22.75,
"outer_size_z_m": 16.25,
"table_count": 35,
"aisle_count": 12,
"narrow_aisle_count": 10
},
"conditions": [
{"condition_id": "w080_empty_r1", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "empty", "walkers": 3, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w080_empty_r2", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "empty", "walkers": 3, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w080_normal_r1", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "normal", "walkers": 6, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w080_normal_r2", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "normal", "walkers": 6, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w080_busy_r1", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "busy", "walkers": 10, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w080_busy_r2", "aisle_width_m": 0.8, "aisle_width_index": 0, "crowd": "busy", "walkers": 10, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_empty_r1", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "empty", "walkers": 3, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_empty_r2", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "empty", "walkers": 3, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_normal_r1", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "normal", "walkers": 6, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_normal_r2", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "normal", "walkers": 6, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_busy_r1", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "busy", "walkers": 10, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w100_busy_r2", "aisle_width_m": 1, "aisle_width_index": 1, "crowd": "busy", "walkers": 10, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_empty_r1", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "empty", "walkers": 3, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_empty_r2", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "empty", "walkers": 3, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_normal_r1", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "normal", "walkers": 6, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_normal_r2", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "normal", "walkers": 6, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_busy_r1", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "busy", "walkers": 10, "robots": 1, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true},
{"condition_id": "w120_busy_r2", "aisle_width_m": 1.2, "aisle_width_index": 2, "crowd": "busy", "walkers": 10, "robots": 2, "trips": 100, "seeds": [0, 1, 2, 3, 4], "aisle_yielding": true}
]
}
trips.csv
condition_id,aisle_width_m,crowd,robots,seed,robot_id,trip_id,table_id,distance_m,total_s,stopped_s,n_stops,stop_durations_s,stuck,timeout,wait_aisle_s,max_decel_mps2,min_clearance_m,met_robot,table_ids,dishes,dish_ready_s,dish_arrived_s,undelivered
w100_normal_r2,1,normal,2,0,1,1,30,22.6501,55.3494,15.4,4,0.95;11.7;0.6;2.15,0,0,0,1,0.0527,0,30,3,372.6025;612.344;408.6437,922.6182;922.6182;922.6182,0
w100_normal_r2,1,normal,2,0,1,2,31,8.61,102.5017,87.0507,2,0.95;11.05,1,0,0,1,0.0023,0,31,1,965.1578,1045.564,0
w100_normal_r2,1,normal,2,0,1,3,31,4.95,84.8006,76,1,0.95,1,0,0,1,-0.0491,0,31;29;21,1,1009.047,-1,1
w100_normal_r2,1,normal,2,0,1,4,31,4.95,85.9507,76.6001,2,0.95;0.6,1,0,0,1,0.359,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,5,31,4.95,126.8031,117.4526,2,0.95;41.4497,1,0,0,1,0.0223,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,6,31,4.95,84.8006,76,1,0.95,1,0,0,1,2.4597,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,7,31,4.95,84.8006,76,1,0.95,1,0,0,1,-0.0455,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,8,32,4.7175,600.027,591.2292,1,0.95,0,1,590.2794,1,0.0008,0,32;30;5,2,734.2142;689.4752,1070.239;-1,1
w100_normal_r2,1,normal,2,0,1,9,31,4.95,84.8006,76,1,0.95,1,0,0,1,-0.0601,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,10,31,4.95,87.1507,77.8002,2,0.95;1.8,1,0,0,1,0.069,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,11,31,4.95,84.8006,76,1,0.95,1,0,0,1,1.2165,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,12,31,4.8,84.5506,76,1,0.95,1,0,0,1,-0.038,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,13,31,4.95,84.8006,76,1,0.95,1,0,0,1,0.1386,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,14,31,4.95,84.8006,76,1,0.95,1,0,0,1,-0.0516,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,15,31,7.86,143.2041,128.4532,3,0.95;21.1;31.3498,1,0,0,1,0.3308,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,16,32,11.1275,600.027,578.8322,3,0.95;10.9;14.55,0,1,552.4387,1,-0.0183,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,17,31,4.95,85.2506,76.4501,1,1.4,1,0,0,1,0.0223,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,18,31,4.95,105.2518,95.9013,2,0.95;19.9,1,0,0,1,0,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,19,31,4.95,86.2007,76.8501,2,0.95;0.85,1,0,0,1,0.0059,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,20,31,4.95,92.301,82.9505,2,0.95;6.95,1,0,0,1,-0.0281,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,21,31,5.79,132.1035,120.8028,3,0.95;13.7;31.0998,1,0,0,1,-0.0595,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,22,32,9.125,600.027,582.3314,4,2.6;0.05;12.1;9.6,0,1,557.9873,1,-0.0143,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,23,31,9.6,104.3018,87.2007,2,0.95;11.2,1,0,0,1,-0.0988,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,24,31,4.95,92.9511,84.1505,1,9.1,1,0,0,1,0.1693,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,25,31,6.06,129.9033,118.1526,3,0.95;21.1;21.05,1,0,0,1,-0.0258,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,26,32,27.3777,393.5774,343.3897,6,0.95;2.4;12.1;21.55;11.1;1.8,1,0,218.4587,1,-0.0238,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,27,31,29.2227,212.0583,159.0551,8,0.95;1.65;11.5;11;11.35;4.15;2.3;41.0997,1,0,0,1,0.0054,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,28,31,0,75.05,75.05,0,,1,0,0,0,0.1619,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,29,31,0,75.05,75.05,0,,1,0,0,0,-0.45,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,30,31,5.79,160.0552,148.7545,3,41.7997;0.75;31.1498,1,0,0,1,-0.45,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,31,32,20.5226,423.0202,384.8296,4,45.2496;3.4;11.1;10.9,1,0,239.16,1,-0.175,0,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,32,31,8.8125,149.8545,134.0036,1,0.95,1,0,57.9994,1,0.0014,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,33,31,4.95,84.8006,76,1,0.95,1,0,0,1,1.2309,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,34,31,4.95,85.0506,76.2501,1,1.2,1,0,0,1,-0.0455,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,35,31,8.02,176.8062,160.7052,5,0.95;11;1.2;21.05;51.4495,1,0,0,1,0.0093,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,36,31,4.95,85.1506,76.3501,1,1.3,1,0,0,1,0.3082,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,37,32,4.8925,600.027,590.3794,3,0.95;0.05;15.25,0,1,574.1334,1,-0.0147,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,38,31,5.55,109.2021,98.3014,3,0.95;1.2;21.1,1,0,0,1,-0.036,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,39,31,4.95,84.8006,76,1,0.95,1,0,0,1,0.359,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,40,31,4.95,85.1506,76.3501,1,1.3,1,0,0,1,0.128,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,41,31,4.95,84.8006,76,1,0.95,1,0,0,1,0.2266,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,42,31,4.95,84.8006,76,1,0.95,1,0,0,1,0.6889,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,43,31,4.95,86.2507,76.9001,2,0.95;0.9,1,0,0,1,0.2402,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,44,31,4.95,84.8006,76,1,0.95,1,0,0,1,2.1481,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,45,32,7.8375,600.027,585.4806,2,0.95;13.1,0,1,571.434,1,-0.0151,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,46,31,4.95,84.8006,76,1,0.95,1,0,0,1,0.233,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,47,31,4.95,86.7507,77.4001,2,1.35;1,1,0,0,1,0.1547,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,48,31,4.95,86.7507,77.4001,2,0.95;1.4,1,0,0,1,-0.046,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,49,31,4.95,84.8006,76,1,0.95,1,0,0,1,1.0631,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,50,31,4.95,84.8006,76,1,0.95,1,0,0,1,1.2039,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,51,31,4.95,91.901,82.5504,2,0.95;6.55,1,0,0,1,-0.0252,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,52,31,4.95,87.0007,77.6501,2,0.95;1.65,1,0,0,1,0.0591,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,53,32,4.72,600.027,591.1792,1,0.95,0,1,590.2294,1,0.0005,0,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,54,31,4.95,96.9013,87.0007,3,0.95;6.55;4.45,1,0,0,1,0.0726,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,55,31,4.95,88.6508,79.3002,2,0.95;3.3,1,0,0,1,0.1522,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,56,31,4.95,84.8006,76,1,0.95,1,0,0,1,0.2402,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,57,31,4.95,84.8006,76,1,0.95,1,0,0,1,0.176,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,58,31,4.95,84.8006,76,1,0.95,1,0,0,1,1.0739,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,59,31,5.79,137.9538,127.2032,2,0.95;51.1995,1,0,0,1,-0.0237,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,60,32,8.74,600.027,583.4311,2,0.95;12.85,0,1,569.6345,1,-0.0144,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,61,31,4.95,92.7511,83.4005,2,0.95;7.4,1,0,0,1,0.235,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,62,31,4.95,84.8006,76,1,0.95,1,0,0,1,-0.0459,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,63,31,5.8825,164.4054,151.9047,5,0.95;1.7;51.6995;0.85;21.65,1,0,0,1,-0.0936,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,64,32,14.5175,283.8042,257.8106,2,0.95;31.3498,1,0,150.4546,1,-0.0004,0,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,65,32,11.6625,170.0558,148.9045,2,0.95;21.05,1,0,51.8495,1,0.0034,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,66,31,13.1325,199.5576,174.8561,4,0.95;1.35;51.6995;31.6998,1,0,14.1,1,0.0075,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,67,32,0.3025,77.0501,76,1,0.95,1,0,0,1,0.0731,0,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,68,31,0,75.05,75.05,0,,1,0,0,0,0.2493,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,69,32,0.3025,77.6001,76.5501,1,1.5,1,0,0,1,0.0062,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,70,31,0,75.05,75.05,0,,1,0,0,0,0.1967,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,71,31,8.76,134.9036,119.2027,2,42.1496;2,1,0,0,1,-0.45,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,72,31,4.95,84.8006,76,1,0.95,1,0,0,1,0.0223,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,73,31,4.95,84.8006,76,1,0.95,1,0,0,1,-0.0455,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,74,31,4.95,87.9008,78.5502,2,0.95;2.55,1,0,0,1,-0.0581,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,75,31,4.95,84.8006,76,1,0.95,1,0,0,1,1.1354,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,76,31,4.95,111.9522,102.6017,2,0.95;26.5999,1,0,0,1,0.2217,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,77,32,4.7175,600.027,590.6793,2,43.5996;24.1499,0,1,522.9459,1,-0.45,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,78,31,4.95,84.8006,76,1,0.95,1,0,0,1,0.7441,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,79,31,4.95,84.8006,76,1,0.95,1,0,0,1,1.2016,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,80,31,5.76,98.1514,87.4507,2,0.95;11.45,1,0,0,1,0.3305,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,81,31,4.95,98.0014,89.2008,1,14.15,1,0,0,1,0.0223,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,82,31,4.95,84.8006,76,1,0.95,1,0,0,1,0.0223,0,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,83,31,5.76,149.5045,138.2538,3,0.95;41.1997;21.05,1,0,0,1,-0.0223,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,84,32,8.8975,600.027,582.2814,3,0.95;15.15;12.3,0,1,553.8883,1,-0.015,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,1,85,31,11.07,171.5059,150.8546,4,0.95;1.65;51.8995;21.3,1,0,0,1,0.1668,1,31;29;21,3,1009.047;1065.884;1089.207,-1;-1;-1,3
w100_normal_r2,1,normal,2,0,2,86,32,6.46,163.4554,150.7546,3,0.95;5.25;29.4998,1,0,39.9997,1,-0.0317,1,32;30;5,3,1054.573;1051.02;689.4752,-1;-1;-1,3
stops.csv
condition_id,seed,robot_id,trip_id,t_s,x,z,aisle,cause,cause_detail,path_blocked,duration_s,resolved,clearance_left_cm,clearance_right_cm,min_gap_cm,blocked_s,no_path_s,yield_s,clear_wait_s,replans
w100_normal_r2,0,1,1,965.35,11.3631,13.076,MainAisle,分からない,,0,0.95,自分で動き出した,78.9563,35.2241,35.2241,0,0,0,0.95,0
w100_normal_r2,0,1,1,974.5,7.478,11.3278,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,11.7,経路を引き直した,0,97.9035,0,10.75,0,0,0.95,1
w100_normal_r2,0,1,1,1003.05,7.4632,7.6507,ColumnAisle02_N,椅子・荷物・ベビーカー,Table30/Chair,0,0.6,自分で動き出した,275,0,0,0,0,0,0.6,0
w100_normal_r2,0,1,1,1038.1,8.7102,11.8906,NorthAisle,スタッフとすれ違った,Staff01,0,2.15,自分で動き出した,26.3803,24.4833,24.4833,1.2,0,0,0.95,0
w100_normal_r2,0,1,2,1045.75,11.3637,13.0771,MainAisle,分からない,,0,0.95,自分で動き出した,78.9777,35.1359,35.1359,0,0,0,0.95,0
w100_normal_r2,0,1,2,1048.95,10.7544,12.35,MainAisle,人が立ち止まっていた,Staff01,0,11.05,経路を引き直した,223.0589,58.9126,58.9126,10.1,0,0,0.95,1
w100_normal_r2,0,1,2,1073.2,7.4745,7.6546,ColumnAisle02_N,椅子・荷物・ベビーカー,Table33/Chair,0,60.0494,動けなくなった,275,5.3524,5.3524,60.0494,0,0,0,5
w100_normal_r2,0,1,3,1148.25,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.1818,55.1817,55.1817,0,0,0,0.95,0
w100_normal_r2,0,1,3,1158,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,4,1233.05,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,4,1236.75,10.8131,12.3842,MainAisle,お客さんとすれ違った,Guest(Clone),0,0.6,自分で動き出した,209.6444,70.5652,70.5652,0,0,0,0.6,0
w100_normal_r2,0,1,4,1243.95,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,5,1319,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,5,1325.15,9.4152,12.004,NorthAisle,人が立ち止まっていた,Staff02,0,41.4497,経路を引き直した,275,176.6984,176.6984,40.4997,0,0,0.95,4
w100_normal_r2,0,1,5,1370.75,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,6,1445.8,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,6,1455.55,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,7,1530.6,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,7,1540.35,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,2,8,1070.4,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,275,126.0949,126.0949,0,0,0,0.95,0
w100_normal_r2,0,2,8,1079.8,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,68.3996,入口で待った,99.8641,112.7179,99.8641,0,0,68.3996,0,0
w100_normal_r2,0,2,8,1148.25,10.7132,8.799,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,104.3568,117.5833,104.3568,0,0,84.7506,0,0
w100_normal_r2,0,2,8,1233.05,10.7128,8.8015,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,85.9006,入口で待った,112.6974,151.1982,112.6974,0,0,85.9006,0,0
w100_normal_r2,0,2,8,1319,10.7124,8.8039,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,126.7531,入口で待った,126.1581,141.4042,126.1581,0,0,126.7531,0,0
w100_normal_r2,0,2,8,1445.8,10.712,8.8064,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,147.2219,164.4733,147.2219,0,0,84.7506,0,0
w100_normal_r2,0,2,8,1530.6,10.7116,8.8089,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,180.9758,201.4656,180.9758,0,0,84.7506,0,0
w100_normal_r2,0,2,8,1615.4,10.7112,8.8113,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,55.0495,入口で待った,239.233,275,239.233,0,0,55.0495,0,0
w100_normal_r2,0,1,9,1615.4,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,9,1625.15,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,10,1700.2,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,10,1704.15,10.6818,12.3115,MainAisle,スタッフとすれ違った,Staff02,0,1.8,自分で動き出した,236.7232,43.4845,43.4845,0.85,0,0,0.95,0
w100_normal_r2,0,1,10,1712.3,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,11,1787.35,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,11,1797.1,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,12,1872.15,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,12,1881.65,7.5016,11.4776,NorthAisle,人が立ち止まっていた,Staff02,0,60.0494,動けなくなった,7.6752,222.2653,7.6752,60.0494,0,0,0,5
w100_normal_r2,0,1,13,1956.7,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,106.7369,106.7371,106.7369,0,0,0,0.95,0
w100_normal_r2,0,1,13,1966.45,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,14,2041.5,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,14,2051.25,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,15,2126.3,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,15,2134.1,10.6404,9.7445,MainAisle,もう1台のロボット,ServingRobot02,0,21.1,経路を引き直した,108.5777,106.6093,106.6093,20.15,0,0,0.95,2
w100_normal_r2,0,1,15,2159.1,10.1412,11.6737,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,31.3498,経路を引き直した,72.5801,198.0922,72.5801,30.3998,0,0,0.95,3
w100_normal_r2,0,1,15,2194.45,10.2048,9.6109,ColumnAisle03_N,もう1台のロボット,ServingRobot02,0,60.0494,動けなくなった,149.572,60.509,60.509,60.0494,0,0,0,5
w100_normal_r2,0,2,16,1670.5,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,145.6363,144.8203,144.8203,0,0,0,0.95,0
w100_normal_r2,0,2,16,1679.9,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,20.25,入口で待った,99.8641,112.7179,99.8641,0,0,20.25,0,0
w100_normal_r2,0,2,16,1700.2,10.7132,8.799,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,87.1007,入口で待った,104.3568,117.5833,104.3568,0,0,87.1007,0,0
w100_normal_r2,0,2,16,1787.35,10.7128,8.8015,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,112.6974,151.1982,112.6974,0,0,84.7506,0,0
w100_normal_r2,0,2,16,1872.15,10.7124,8.8039,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,69.4996,入口で待った,126.1581,141.4042,126.1581,0,0,69.4996,0,0
w100_normal_r2,0,2,16,1947.1,10.1415,11.6677,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,10.9,経路を引き直した,71.2161,194.8503,71.2161,9.95,0,0,0.95,1
w100_normal_r2,0,2,16,1963.45,10.6819,8.7979,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,78.0002,入口で待った,103.9436,110.4397,103.9436,0,0,78.0002,0,0
w100_normal_r2,0,2,16,2041.5,10.6816,8.8004,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,109.5188,116.213,109.5188,0,0,84.7506,0,0
w100_normal_r2,0,2,16,2126.3,10.6806,8.7981,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,128.1532,入口で待った,104.083,110.3004,104.083,0,0,128.1532,0,0
w100_normal_r2,0,2,16,2255.9,10.5986,9.3318,MainAisle,もう1台のロボット,ServingRobot01,0,14.55,経路を引き直した,101.3208,111.7313,101.3208,13.6,0,0,0.95,1
w100_normal_r2,0,1,17,2269.5,11.15,13.3,MainAisle,分からない,,0,1.4,自分で動き出した,55.0249,55.025,55.0249,0.4,0,0,1,0
w100_normal_r2,0,1,17,2279.7,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,18,2354.75,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,18,2363.8,7.7185,11.637,NorthAisle,お客さんとすれ違った,Guest(Clone),0,19.9,経路を引き直した,7.6783,102.0686,7.6783,18.95,0,0,0.95,1
w100_normal_r2,0,1,18,2384.95,7.4819,11.332,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,19,2460,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.207,55.2071,55.207,0,0,0,0.95,0
w100_normal_r2,0,1,19,2463.6,10.8655,12.4133,MainAisle,スタッフとすれ違った,Staff01,0,0.85,自分で動き出した,198.8126,81.3976,81.3976,0,0,0,0.85,0
w100_normal_r2,0,1,19,2471.15,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,20,2546.2,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,20,2554.55,8.1194,11.762,NorthAisle,人が立ち止まっていた,Guest(Clone),0,6.95,自分で動き出した,12.9239,199.7953,12.9239,6,0,0,0.95,0
w100_normal_r2,0,1,20,2563.45,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,21,2638.5,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,21,2644.85,10.8116,10.5975,MainAisle,人が立ち止まっていた,Guest(Clone),0,13.7,経路を引き直した,91.1138,124.073,91.1138,12.75,0,0,0.95,1
w100_normal_r2,0,1,21,2660.55,10.6404,9.7445,MainAisle,もう1台のロボット,ServingRobot02,0,31.0998,経路を引き直した,108.578,106.6089,106.6089,30.1498,0,0,0.95,3
w100_normal_r2,0,1,21,2695.55,10.1412,11.6737,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,60.0494,動けなくなった,72.5799,198.0916,72.5799,60.0494,0,0,0,5
w100_normal_r2,0,2,22,2270.6,11.15,13.3,MainAisle,人が立ち止まっていた,Staff02,0,2.6,自分で動き出した,56.0246,56.0247,56.0246,1.65,0,0,0.95,0
w100_normal_r2,0,2,22,2281.65,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,73.0499,入口で待った,99.8641,112.7179,99.8641,0,0,73.0499,0,0
w100_normal_r2,0,2,22,2354.75,10.7132,8.799,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,105.2018,入口で待った,104.3568,117.5833,104.3568,0,0,105.2018,0,0
w100_normal_r2,0,2,22,2460,10.7128,8.8015,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,86.1507,入口で待った,112.6974,151.1982,112.6974,0,0,86.1507,0,0
w100_normal_r2,0,2,22,2546.2,10.7124,8.8039,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,92.251,入口で待った,126.1581,141.4042,126.1581,0,0,92.251,0,0
w100_normal_r2,0,2,22,2638.5,10.7124,8.8039,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,117.0525,入口で待った,147.1651,164.5301,147.1651,0,0,117.0525,0,0
w100_normal_r2,0,2,22,2759.45,10.3845,10.787,MainAisle,もう1台のロボット,ServingRobot01,0,12.1,経路を引き直した,79.8499,133.6815,79.8499,11.15,0,0,0.95,1
w100_normal_r2,0,2,22,2775.5,10.6857,8.7946,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.3506,入口で待った,102.8675,110.0873,102.8675,0,0,84.3506,0,0
w100_normal_r2,0,2,22,2861.05,10.6258,9.18,MainAisle,もう1台のロボット,ServingRobot01,0,9.6,自分で動き出した,104.1136,109.0033,104.1136,9.6,0,0,0,0
w100_normal_r2,0,1,23,2770.6,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,89.0851,73.6681,73.6681,0,0,0,0.95,0
w100_normal_r2,0,1,23,2780.05,7.5254,11.4959,NorthAisle,お客さんとすれ違った,Guest(Clone),0,11.2,経路を引き直した,3.7503,226.1896,3.7503,10.25,0,0,0.95,1
w100_normal_r2,0,1,23,2799.85,10.2119,9.6047,ColumnAisle03_N,もう1台のロボット,ServingRobot02,0,60.0494,動けなくなった,148.8516,61.2065,61.2065,60.0494,0,0,0,5
w100_normal_r2,0,1,24,2874.9,11.15,13.3,MainAisle,もう1台のロボット,ServingRobot02,0,9.1,自分で動き出した,55.0178,55.0179,55.0178,7.4,0,0,1.7,0
w100_normal_r2,0,1,24,2892.8,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,25,2967.85,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,25,2971.7,10.7343,12.3406,MainAisle,人が立ち止まっていた,Staff02,0,21.1,経路を引き直した,225.8917,54.3165,54.3165,20.15,0,0,0.95,2
w100_normal_r2,0,1,25,2997.75,10.4543,9.7155,MainAisle,もう1台のロボット,ServingRobot02,0,21.05,経路を引き直した,125.4168,86.0582,86.0582,20.1,0,0,0.95,2
w100_normal_r2,0,1,25,3022.7,10.1349,11.6758,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,60.0494,動けなくなった,70.086,194.8786,70.086,60.0494,0,0,0,5
w100_normal_r2,0,2,26,2870.7,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.9591,55.959,55.959,0,0,0,0.95,0
w100_normal_r2,0,2,26,2873.25,11.2827,12.8291,MainAisle,スタッフとすれ違った,Staff01,0,2.4,自分で動き出した,42.4051,69.2193,42.4051,1.45,0,0,0.95,0
w100_normal_r2,0,2,26,2883.05,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,99.8634,112.7127,99.8634,0,0,84.7506,0,0
w100_normal_r2,0,2,26,2967.85,10.7132,8.799,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,114.8524,入口で待った,104.3527,117.5742,104.3527,0,0,114.8524,0,0
w100_normal_r2,0,2,26,3086.6,10.3853,10.782,MainAisle,もう1台のロボット,ServingRobot01,0,12.1,経路を引き直した,79.9331,133.5982,79.9331,11.15,0,0,0.95,1
w100_normal_r2,0,2,26,3102.65,10.6846,8.802,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,18.85,入口で待った,102.9795,79.6332,79.6332,0,0,18.85,0,0
w100_normal_r2,0,2,26,3128.9,7.4817,7.6526,ColumnAisle02_N,椅子・荷物・ベビーカー,Table33/Chair,0,21.55,経路を引き直した,275,25.7297,25.7297,20.6,0,0,0.95,2
w100_normal_r2,0,2,26,3160.9,10.2321,10.7512,ColumnAisle03_N,もう1台のロボット,ServingRobot01,0,11.1,経路を引き直した,108.2363,146.8319,108.2363,10.15,0,0,0.95,1
w100_normal_r2,0,2,26,3185.2,5.6773,7.351,CrossAisle,スタッフとすれ違った,Staff02,0,1.8,自分で動き出した,70.1038,0,0,0.85,0,0,0.95,0
w100_normal_r2,0,2,26,3189.25,4.8706,7.7127,ColumnAisle01_N,椅子・荷物・ベビーカー,Table30/Chair,0,60.0494,動けなくなった,57.0774,0,0,59.8994,0,0,0.15,5
w100_normal_r2,0,1,27,3097.75,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,87.0545,71.912,71.912,0,0,0,0.95,0
w100_normal_r2,0,1,27,3101.2,10.9443,12.4569,MainAisle,スタッフとすれ違った,Staff02,0,1.65,自分で動き出した,182.5647,97.6461,97.6461,0.7,0,0,0.95,0
w100_normal_r2,0,1,27,3109.7,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,11.5,経路を引き直した,0,275,0,10.55,0,0,0.95,1
w100_normal_r2,0,1,27,3136.3,8.4042,7.3555,CrossAisle,もう1台のロボット,ServingRobot02,0,11,経路を引き直した,70.5637,0,0,10.05,0,0,0.95,1
w100_normal_r2,0,1,27,3157.55,10.1281,11.6751,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,11.35,経路を引き直した,66.7907,189.9137,66.7907,10.4,0,0,0.95,1
w100_normal_r2,0,1,27,3169.55,10.152,11.5646,NorthAisle,もう1台のロボット,ServingRobot02,0,4.15,自分で動き出した,154.866,275,154.866,3.2,0,0,0.95,0
w100_normal_r2,0,1,27,3186.75,6.5866,7.3553,CrossAisle,もう1台のロボット,ServingRobot02,0,2.3,自分で動き出した,78.0287,6.9728,6.9728,1.2,0,0,1.1,0
w100_normal_r2,0,1,27,3190.95,5.7766,7.3515,CrossAisle,もう1台のロボット,ServingRobot02,0,41.0997,経路を引き直した,70.1527,0,0,40.1497,0,0,0.95,4
w100_normal_r2,0,1,27,3234.75,7.0311,7.5458,CrossAisle,椅子・荷物・ベビーカー,Table33/Chair,0,60.0494,動けなくなった,55.1456,136.2349,55.1456,60.0494,0,0,0,5
w100_normal_r2,0,1,28,3309.8,11.15,13.3,MainAisle,もう1台のロボット,ServingRobot02,0,60.0494,動けなくなった,139.8573,167.8286,139.8573,60.0494,0,0,0,5
w100_normal_r2,0,1,29,3384.85,11.15,13.3,MainAisle,人が立ち止まっていた,Staff02,0,60.0494,動けなくなった,125,275,125,60.0494,0,0,0,5
w100_normal_r2,0,1,30,3459.9,11.15,13.3,MainAisle,人が立ち止まっていた,Staff01,0,41.7997,経路を引き直した,125,275,125,40.7997,0,0,1,4
w100_normal_r2,0,1,30,3502.95,11.3016,13.0388,MainAisle,スタッフとすれ違った,Staff02,0,0.75,自分で動き出した,41.1312,72.0606,41.1312,0,0,0,0.75,0
w100_normal_r2,0,1,30,3509.85,10.6404,9.7445,MainAisle,もう1台のロボット,ServingRobot02,0,31.1498,経路を引き直した,108.5778,106.6092,106.6092,30.1998,0,0,0.95,3
w100_normal_r2,0,1,30,3544.9,10.1412,11.6737,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,60.0494,動けなくなった,72.5799,198.0916,72.5799,60.0494,0,0,0,5
w100_normal_r2,0,2,31,3264.3,11.15,13.3,MainAisle,分からない,,0,45.2496,経路を引き直した,69.4021,69.4022,69.4021,43.8996,0,0,1.35,4
w100_normal_r2,0,2,31,3310.9,11.3043,12.9775,MainAisle,スタッフとすれ違った,Staff02,0,3.4,自分で動き出した,40.2085,71.3804,40.2085,1.8,0,0,1.6,0
w100_normal_r2,0,2,31,3321.95,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,47.8496,入口で待った,99.8641,102.5858,99.8641,0,0,47.8496,0,0
w100_normal_r2,0,2,31,3375.25,10.1507,11.6622,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,11.1,経路を引き直した,74.171,197.8531,74.171,9.75,0,0,1.35,1
w100_normal_r2,0,2,31,3391.8,10.6822,8.7961,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,53.0495,入口で待った,103.9421,110.4897,103.9421,0,0,53.0495,0,0
w100_normal_r2,0,2,31,3450.3,10.1485,11.6661,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,10.9,経路を引き直した,74.1212,198.6331,74.1212,9.95,0,0,0.95,1
w100_normal_r2,0,2,31,3466.65,10.6813,8.801,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,138.2538,入口で待った,104.0317,110.396,104.0317,0,0,138.2538,0,0
w100_normal_r2,0,2,31,3612.3,7.4805,7.6532,ColumnAisle02_N,椅子・荷物・ベビーカー,Table33/Chair,0,60.0494,動けなくなった,98.1576,27.5038,27.5038,60.0494,0,0,0,5
w100_normal_r2,0,1,32,3619.95,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,103.1339,73.6681,73.6681,0,0,0,0.95,0
w100_normal_r2,0,1,32,3629.35,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot02,0,57.9994,入口で待った,99.8641,112.7179,99.8641,0,0,57.9994,0,0
w100_normal_r2,0,1,32,3694.75,7.4856,7.6491,ColumnAisle02_N,椅子・荷物・ベビーカー,Table33/Chair,0,60.0494,動けなくなった,275,10.4606,10.4606,60.0494,0,0,0,5
w100_normal_r2,0,1,33,3769.8,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2145,55.2144,55.2144,0,0,0,0.95,0
w100_normal_r2,0,1,33,3779.55,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,34,3854.6,11.15,13.3,MainAisle,スタッフとすれ違った,Staff01,0,1.2,自分で動き出した,55.2065,55.2066,55.2065,0.25,0,0,0.95,0
w100_normal_r2,0,1,34,3864.6,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,35,3939.65,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,35,3943.1,10.9443,12.4569,MainAisle,椅子・荷物・ベビーカー,Table14/Chair,0,11,経路を引き直した,182.5647,97.6461,97.6461,10.05,0,0,0.95,1
w100_normal_r2,0,1,35,3955.05,10.9074,12.2096,MainAisle,スタッフとすれ違った,Staff01,0,1.2,自分で動き出した,80.4119,43.9946,43.9946,0.25,0,0,0.95,0
w100_normal_r2,0,1,35,3960.95,10.5402,9.7468,MainAisle,もう1台のロボット,ServingRobot02,0,21.05,経路を引き直した,117.5339,95.3397,95.3397,20.1,0,0,0.95,2
w100_normal_r2,0,1,35,3985.85,10.1482,11.6672,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,51.4495,経路を引き直した,74.242,199.0378,74.242,50.4995,0,0,0.95,5
w100_normal_r2,0,1,35,4041.4,10.2051,9.539,ColumnAisle03_N,もう1台のロボット,ServingRobot02,0,60.0494,動けなくなった,149.5456,105.556,105.556,60.0494,0,0,0,5
w100_normal_r2,0,1,36,4116.45,11.15,13.3,MainAisle,人が立ち止まっていた,Staff01,0,1.3,自分で動き出した,55.0266,55.0267,55.0266,0.35,0,0,0.95,0
w100_normal_r2,0,1,36,4126.55,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,2,37,3687.35,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.0689,55.0688,55.0688,0,0,0,0.95,0
w100_normal_r2,0,2,37,3696.75,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,72.9999,入口で待った,99.8641,112.7179,99.8641,0,0,72.9999,0,0
w100_normal_r2,0,2,37,3769.8,10.7132,8.799,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,104.3568,117.5833,104.3568,0,0,84.7506,0,0
w100_normal_r2,0,2,37,3854.6,10.7128,8.8015,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,85.0006,入口で待った,112.6974,151.1982,112.6974,0,0,85.0006,0,0
w100_normal_r2,0,2,37,3939.65,10.7128,8.8015,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,161.7553,入口で待った,126.1082,141.454,126.1082,0,0,161.7553,0,0
w100_normal_r2,0,2,37,4102.15,10.6867,8.9593,MainAisle,もう1台のロボット,ServingRobot01,0,15.25,経路を引き直した,110.7248,103.269,103.269,14.3,0,0,0.95,1
w100_normal_r2,0,2,37,4117.65,10.6879,8.9368,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,83.9005,入口で待った,101.3791,108.9621,101.3791,0,0,83.9005,0,0
w100_normal_r2,0,2,37,4201.6,10.687,8.9345,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,85.8006,入口で待った,102.0123,109.4471,102.0123,0,0,85.8006,0,0
w100_normal_r2,0,1,38,4201.6,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,38,4208,10.8057,10.5681,MainAisle,スタッフとすれ違った,Staff02,0,1.2,自分で動き出した,91.716,130.9333,91.716,0.25,0,0,0.95,0
w100_normal_r2,0,1,38,4210.95,10.664,9.8622,MainAisle,もう1台のロボット,ServingRobot02,0,21.1,経路を引き直した,106.1692,109.0178,106.1692,20.15,0,0,0.95,2
w100_normal_r2,0,1,38,4235.75,10.1527,11.6659,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,60.0494,動けなくなった,76.0829,201.3477,76.0829,60.0494,0,0,0,5
w100_normal_r2,0,1,39,4310.8,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,91.49,75.7481,75.7481,0,0,0,0.95,0
w100_normal_r2,0,1,39,4320.55,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,40,4395.6,11.15,13.3,MainAisle,スタッフとすれ違った,Staff01,0,1.3,自分で動き出した,55.2065,55.2066,55.2065,0.35,0,0,0.95,0
w100_normal_r2,0,1,40,4405.7,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,41,4480.75,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,41,4490.5,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,42,4565.55,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,42,4575.3,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,43,4650.35,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,43,4654.35,10.6556,12.297,MainAisle,スタッフとすれ違った,Staff02,0,0.9,自分で動き出した,242.1388,38.2205,38.2205,0,0,0,0.9,0
w100_normal_r2,0,1,43,4661.55,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,44,4736.6,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,44,4746.35,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,2,45,4287.45,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.1234,55.1235,55.1234,0,0,0,0.95,0
w100_normal_r2,0,2,45,4298.65,10.4327,10.7863,MainAisle,もう1台のロボット,ServingRobot01,0,13.1,経路を引き直した,55.2144,130.5093,55.2144,12.15,0,0,0.95,1
w100_normal_r2,0,2,45,4315.7,10.6872,8.8,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,79.8503,入口で待った,102.3143,109.812,102.3143,0,0,79.8503,0,0
w100_normal_r2,0,2,45,4395.6,10.6868,8.8025,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,85.1006,入口で待った,101.3756,108.7386,101.3756,0,0,85.1006,0,0
w100_normal_r2,0,2,45,4480.75,10.6864,8.805,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,103.5934,111.005,103.5934,0,0,84.7506,0,0
w100_normal_r2,0,2,45,4565.55,10.686,8.8074,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,109.2529,116.9064,109.2529,0,0,84.7506,0,0
w100_normal_r2,0,2,45,4650.35,10.6856,8.8099,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,86.2007,入口で待った,119.1408,134.337,119.1408,0,0,86.2007,0,0
w100_normal_r2,0,2,45,4736.6,10.6853,8.8124,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,134.8408,143.7527,134.8408,0,0,84.7506,0,0
w100_normal_r2,0,2,45,4821.4,10.6849,8.8148,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,66.0994,入口で待った,159.4609,175.6441,159.4609,0,0,66.0994,0,0
w100_normal_r2,0,1,46,4821.4,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,46,4831.15,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,47,4906.2,11.15,13.3,MainAisle,スタッフとすれ違った,Staff01,0,1.35,自分で動き出した,55.2065,55.2066,55.2065,0.4,0,0,0.95,0
w100_normal_r2,0,1,47,4914.7,8.2628,11.8059,NorthAisle,スタッフとすれ違った,Staff02,0,1,自分で動き出した,17.5071,204.7527,17.5071,0.05,0,0,0.95,0
w100_normal_r2,0,1,47,4917.9,7.4817,11.3328,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,48,4992.95,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2063,55.2064,55.2063,0,0,0,0.95,0
w100_normal_r2,0,1,48,4997.5,10.3626,12.1497,MainAisle,スタッフとすれ違った,Staff02,0,1.4,自分で動き出した,275,45.8183,45.8183,0.45,0,0,0.95,0
w100_normal_r2,0,1,48,5004.65,7.4808,11.3323,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,49,5079.7,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2034,55.2035,55.2034,0,0,0,0.95,0
w100_normal_r2,0,1,49,5089.45,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,50,5164.5,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,50,5174.25,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,51,5249.3,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,51,5255.45,9.4152,12.004,NorthAisle,人が立ち止まっていた,Staff02,0,6.55,自分で動き出した,275,176.6984,176.6984,5.6,0,0,0.95,0
w100_normal_r2,0,1,51,5266.15,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,52,5341.2,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,52,5345.55,10.4719,12.1952,MainAisle,スタッフとすれ違った,Staff02,0,1.65,自分で動き出した,275,25.9528,25.9528,0.7,0,0,0.95,0
w100_normal_r2,0,1,52,5353.15,7.4811,11.3324,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,2,53,4887.55,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,91.6433,91.6434,91.6433,0,0,0,0.95,0
w100_normal_r2,0,2,53,4896.95,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,9.2,入口で待った,99.8641,112.7179,99.8641,0,0,9.2,0,0
w100_normal_r2,0,2,53,4906.2,10.7132,8.799,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,86.7007,入口で待った,104.3568,117.5833,104.3568,0,0,86.7007,0,0
w100_normal_r2,0,2,53,4992.95,10.7128,8.8015,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,86.7007,入口で待った,112.6974,151.1982,112.6974,0,0,86.7007,0,0
w100_normal_r2,0,2,53,5079.7,10.7124,8.8039,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,126.1581,141.4042,126.1581,0,0,84.7506,0,0
w100_normal_r2,0,2,53,5164.5,10.712,8.8064,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,147.2219,164.4733,147.2219,0,0,84.7506,0,0
w100_normal_r2,0,2,53,5249.3,10.7116,8.8089,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,91.851,入口で待った,180.9758,201.4656,180.9758,0,0,91.851,0,0
w100_normal_r2,0,2,53,5341.2,10.7112,8.8113,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,86.9507,入口で待った,239.233,275,239.233,0,0,86.9507,0,0
w100_normal_r2,0,2,53,5428.2,10.7108,8.8138,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,59.3994,入口で待った,275,275,275,0,0,59.3994,0,0
w100_normal_r2,0,1,54,5428.2,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2043,55.2044,55.2043,0,0,0,0.95,0
w100_normal_r2,0,1,54,5434.35,9.4152,12.004,NorthAisle,人が立ち止まっていた,Staff01,0,6.55,自分で動き出した,275,176.6984,176.6984,5.6,0,0,0.95,0
w100_normal_r2,0,1,54,5442.2,8.9699,11.9393,NorthAisle,人が立ち止まっていた,Staff01,0,4.45,自分で動き出した,37.0712,67.0332,37.0712,3.5,0,0,0.95,0
w100_normal_r2,0,1,54,5450.05,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,55,5525.1,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,55,5528.45,10.9968,12.4859,MainAisle,スタッフとすれ違った,Staff01,0,3.3,自分で動き出した,171.7328,108.4786,108.4786,2.15,0,0,1.15,0
w100_normal_r2,0,1,55,5538.7,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,56,5613.75,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,56,5623.5,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,57,5698.55,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,57,5708.3,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,58,5783.35,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,58,5793.1,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,59,5868.15,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,59,5875.95,10.6404,9.7445,MainAisle,もう1台のロボット,ServingRobot02,0,51.1995,経路を引き直した,108.5777,106.6093,106.6093,50.2495,0,0,0.95,5
w100_normal_r2,0,1,59,5931.05,10.1412,11.6737,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,60.0494,動けなくなった,72.5801,198.0922,72.5801,60.0494,0,0,0,5
w100_normal_r2,0,2,60,5487.65,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,221.2412,133.6037,133.6037,0,0,0,0.95,0
w100_normal_r2,0,2,60,5497.05,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,27.9999,入口で待った,99.8641,112.7179,99.8641,0,0,27.9999,0,0
w100_normal_r2,0,2,60,5525.1,10.7132,8.799,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,88.6008,入口で待った,104.3568,117.5833,104.3568,0,0,88.6008,0,0
w100_normal_r2,0,2,60,5613.75,10.7128,8.8015,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,112.6974,151.1982,112.6974,0,0,84.7506,0,0
w100_normal_r2,0,2,60,5698.55,10.7124,8.8039,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,126.1581,141.4042,126.1581,0,0,84.7506,0,0
w100_normal_r2,0,2,60,5783.35,10.712,8.8064,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,147.2219,164.4733,147.2219,0,0,84.7506,0,0
w100_normal_r2,0,2,60,5868.15,10.711,8.8041,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,122.9029,入口で待った,126.3383,141.2302,126.3383,0,0,122.9029,0,0
w100_normal_r2,0,2,60,5994.95,10.384,10.7873,MainAisle,もう1台のロボット,ServingRobot01,0,12.85,経路を引き直した,49.3901,133.7152,49.3901,11.5,0,0,1.35,1
w100_normal_r2,0,2,60,6011.75,10.6856,8.795,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,75.95,入口で待った,102.8791,110.0833,102.8791,0,0,75.95,0,0
w100_normal_r2,0,1,61,6006.1,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,89.0854,73.6684,73.6684,0,0,0,0.95,0
w100_normal_r2,0,1,61,6014.75,7.9476,11.7084,NorthAisle,人が立ち止まっていた,Staff01,0,7.4,自分で動き出した,15.1652,142.0951,15.1652,6.45,0,0,0.95,0
w100_normal_r2,0,1,61,6023.8,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,62,6098.85,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,62,6108.6,7.4808,11.333,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,63,6183.65,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2035,55.2036,55.2035,0,0,0,0.95,0
w100_normal_r2,0,1,63,6187.05,10.9705,12.4714,MainAisle,スタッフとすれ違った,Staff02,0,1.7,自分で動き出した,177.1488,103.0624,103.0624,0.75,0,0,0.95,0
w100_normal_r2,0,1,63,6195.65,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,51.6995,経路を引き直した,0,275,0,50.6995,0,0,1,5
w100_normal_r2,0,1,63,6248.7,7.2079,11.72,NorthAisle,椅子・荷物・ベビーカー,Table12/Chair,0,0.85,自分で動き出した,105.0326,222.6722,105.0326,0,0,0,0.85,0
w100_normal_r2,0,1,63,6250.5,6.9752,11.8115,NorthAisle,人が立ち止まっていた,Staff02,0,21.65,経路を引き直した,50.2152,38.2385,38.2385,20.7,0,0,0.95,2
w100_normal_r2,0,1,63,6273,7.1547,11.7325,NorthAisle,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,161.1916,55.992,55.992,60.0494,0,0,0,5
w100_normal_r2,0,2,64,6087.75,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.9115,55.9116,55.9115,0,0,0,0.95,0
w100_normal_r2,0,2,64,6097.15,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,1.65,入口で待った,99.8641,112.7179,99.8641,0,0,1.65,0,0
w100_normal_r2,0,2,64,6098.85,10.7132,8.799,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,104.3568,117.5833,104.3568,0,0,84.7506,0,0
w100_normal_r2,0,2,64,6183.65,10.7128,8.8015,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,64.0493,入口で待った,112.6974,151.1982,112.6974,0,0,64.0493,0,0
w100_normal_r2,0,2,64,6253.15,10.1489,11.6664,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,31.3498,経路を引き直した,74.4026,199.0995,74.4026,30.3998,0,0,0.95,3
w100_normal_r2,0,2,64,6296.5,7.477,7.655,ColumnAisle02_N,椅子・荷物・ベビーカー,Table33/Chair,0,60.0494,動けなくなった,97.801,24.6324,24.6324,60.0494,0,0,0,5
w100_normal_r2,0,2,65,6371.55,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.0653,55.0652,55.0652,0,0,0,0.95,0
w100_normal_r2,0,2,65,6380.95,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,51.8495,入口で待った,99.8641,112.7179,99.8641,0,0,51.8495,0,0
w100_normal_r2,0,2,65,6442.8,5.7749,7.3514,CrossAisle,もう1台のロボット,ServingRobot01,0,21.05,経路を引き直した,70.1453,0,0,20.1,0,0,0.95,2
w100_normal_r2,0,2,65,6466.55,7.0295,7.5452,CrossAisle,椅子・荷物・ベビーカー,Table33/Chair,0,60.0494,動けなくなった,55.1611,137.0444,55.1611,60.0494,0,0,0,5
w100_normal_r2,0,1,66,6348.05,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,117.498,275,117.498,0,0,0,0.95,0
w100_normal_r2,0,1,66,6357.45,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot02,0,14.1,入口で待った,99.8641,112.7179,99.8641,0,0,14.1,0,0
w100_normal_r2,0,1,66,6376,9.1044,7.3726,CrossAisle,お客さんとすれ違った,Guest(Clone),0,1.35,自分で動き出した,80.8692,5.5651,5.5651,0.4,0,0,0.95,0
w100_normal_r2,0,1,66,6380.85,7.4855,7.6491,ColumnAisle02_N,椅子・荷物・ベビーカー,Table33/Chair,0,51.6995,経路を引き直した,275,10.4219,10.4219,50.7495,0,0,0.95,5
w100_normal_r2,0,1,66,6438,4.8816,7.7072,ColumnAisle01_N,椅子・荷物・ベビーカー,Table30/Chair,0,31.6998,経路を引き直した,58.176,0,0,30.6498,0,0,1.05,3
w100_normal_r2,0,1,66,6472.55,6.0918,7.3575,CrossAisle,もう1台のロボット,,0,60.0494,動けなくなった,0,70.785,0,60.0494,0,0,0,5
w100_normal_r2,0,2,67,6541.6,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,139.7667,168.3409,139.7667,0,0,0,0.95,0
w100_normal_r2,0,2,67,6543.6,11.321,13.1507,MainAisle,人が立ち止まっていた,Staff02,0,60.0494,動けなくなった,38.421,72.903,38.421,60.0494,0,0,0,5
w100_normal_r2,0,1,68,6547.6,11.15,13.3,MainAisle,もう1台のロボット,ServingRobot02,0,60.0494,動けなくなった,125.0624,275,125.0624,60.0494,0,0,0,5
w100_normal_r2,0,2,69,6618.65,11.15,13.3,MainAisle,スタッフとすれ違った,Staff01,0,1.5,自分で動き出した,55.7749,55.775,55.7749,0.55,0,0,0.95,0
w100_normal_r2,0,2,69,6621.2,11.321,13.1507,MainAisle,人が立ち止まっていた,Staff02,0,60.0494,動けなくなった,38.421,72.903,38.421,60.0494,0,0,0,5
w100_normal_r2,0,1,70,6622.65,11.15,13.3,MainAisle,もう1台のロボット,ServingRobot02,0,60.0494,動けなくなった,125,275,125,60.0494,0,0,0,5
w100_normal_r2,0,1,71,6697.7,11.15,13.3,MainAisle,人が立ち止まっていた,Staff02,0,42.1496,経路を引き直した,125,275,125,41.1997,0,0,0.95,4
w100_normal_r2,0,1,71,6741,11.3115,13.0972,MainAisle,もう1台のロボット,ServingRobot02,0,2,自分で動き出した,40.1208,73.0601,40.1208,1.05,0,0,0.95,0
w100_normal_r2,0,1,71,6757.55,7.4855,7.6491,ColumnAisle02_N,椅子・荷物・ベビーカー,Table33/Chair,0,60.0494,動けなくなった,275,10.4259,10.4259,60.0494,0,0,0,5
w100_normal_r2,0,1,72,6832.6,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2142,55.2141,55.2141,0,0,0,0.95,0
w100_normal_r2,0,1,72,6842.35,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,73,6917.4,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,73,6927.15,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,74,7002.2,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,74,7004.45,11.3082,13.0073,MainAisle,スタッフとすれ違った,Staff01,0,2.55,自分で動き出した,39.8337,71.7908,39.8337,1.3,0,0,1.25,0
w100_normal_r2,0,1,74,7015.05,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,75,7090.1,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,75,7099.85,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,76,7174.9,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,76,7181.05,9.4152,12.004,NorthAisle,人が立ち止まっていた,Staff02,0,26.5999,経路を引き直した,275,176.6984,176.6984,25.6499,0,0,0.95,2
w100_normal_r2,0,1,76,7211.8,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,2,77,6696.25,11.15,13.3,MainAisle,分からない,,0,43.5996,経路を引き直した,55.7749,55.775,55.7749,42.5496,0,0,1.05,4
w100_normal_r2,0,2,77,6741,11.3193,13.0958,MainAisle,スタッフとすれ違った,Staff01,0,24.1499,経路を引き直した,38.691,72.8892,38.691,22.3,0,0,1.85,2
w100_normal_r2,0,2,77,6773,10.7139,8.7964,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,59.5494,入口で待った,99.8902,112.8061,99.8902,0,0,59.5494,0,0
w100_normal_r2,0,2,77,6832.6,10.7135,8.7988,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,104.4491,117.7466,104.4491,0,0,84.7506,0,0
w100_normal_r2,0,2,77,6917.4,10.7131,8.8013,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,112.8724,150.1382,112.8724,0,0,84.7506,0,0
w100_normal_r2,0,2,77,7002.2,10.7127,8.8038,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,87.8508,入口で待った,126.4485,141.7958,126.4485,0,0,87.8508,0,0
w100_normal_r2,0,2,77,7090.1,10.7123,8.8062,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,147.6931,165.0736,147.6931,0,0,84.7506,0,0
w100_normal_r2,0,2,77,7174.9,10.7119,8.8087,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,111.9022,入口で待った,181.7682,202.4348,181.7682,0,0,111.9022,0,0
w100_normal_r2,0,2,77,7286.85,10.7115,8.8112,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,9.45,入口で待った,240.701,275,240.701,0,0,9.45,0,0
w100_normal_r2,0,1,78,7286.85,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,78,7296.6,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,79,7371.65,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,79,7381.4,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,80,7456.45,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,80,7462.6,9.4152,12.004,NorthAisle,人が立ち止まっていた,Staff01,0,11.45,経路を引き直した,275,176.6984,176.6984,10.35,0,0,1.1,1
w100_normal_r2,0,1,80,7479.55,10.209,9.5259,ColumnAisle03_N,もう1台のロボット,ServingRobot02,0,60.0494,動けなくなった,149.1469,105.9391,105.9391,60.0494,0,0,0,5
w100_normal_r2,0,1,81,7554.6,11.15,13.3,MainAisle,分からない,,0,14.15,経路を引き直した,55.0225,55.0226,55.0225,13.05,0,0,1.1,1
w100_normal_r2,0,1,81,7577.55,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,82,7652.6,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,82,7662.35,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,60.0494,動けなくなった,0,275,0,60.0494,0,0,0,5
w100_normal_r2,0,1,83,7737.4,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.2065,55.2066,55.2065,0,0,0,0.95,0
w100_normal_r2,0,1,83,7741.25,10.7343,12.3406,MainAisle,人が立ち止まっていた,Staff02,0,41.1997,経路を引き直した,225.8917,54.3165,54.3165,40.2497,0,0,0.95,4
w100_normal_r2,0,1,83,7787.15,10.4702,9.8646,MainAisle,もう1台のロボット,ServingRobot02,0,21.05,経路を引き直した,123.8169,87.6581,87.6581,20.1,0,0,0.95,2
w100_normal_r2,0,1,83,7811.85,10.1374,11.6723,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,60.0494,動けなくなった,70.3959,194.62,70.3959,60.0494,0,0,0,5
w100_normal_r2,0,2,84,7296.35,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,146.623,144.5467,144.5467,0,0,0,0.95,0
w100_normal_r2,0,2,84,7305.75,10.7136,8.7965,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,65.8494,入口で待った,99.8641,112.7179,99.8641,0,0,65.8494,0,0
w100_normal_r2,0,2,84,7371.65,10.7132,8.799,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,104.3568,117.5833,104.3568,0,0,84.7506,0,0
w100_normal_r2,0,2,84,7456.45,10.7128,8.8015,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,83.1005,入口で待った,112.6974,151.1982,112.6974,0,0,83.1005,0,0
w100_normal_r2,0,2,84,7540.4,10.6798,9.0012,MainAisle,もう1台のロボット,ServingRobot01,0,15.15,経路を引き直した,144.3021,103.7473,103.7473,14.2,0,0,0.95,1
w100_normal_r2,0,2,84,7556.1,10.6858,8.9114,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,96.4513,入口で待った,101.7036,108.8815,101.7036,0,0,96.4513,0,0
w100_normal_r2,0,2,84,7652.6,10.6854,8.9139,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,84.7506,入口で待った,101.9737,109.0844,101.9737,0,0,84.7506,0,0
w100_normal_r2,0,2,84,7737.4,10.685,8.9164,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,134.4536,入口で待った,102.0014,109.0312,102.0014,0,0,134.4536,0,0
w100_normal_r2,0,2,84,7875.55,10.3826,10.782,MainAisle,もう1台のロボット,ServingRobot01,0,12.3,経路を引き直した,79.6072,133.7866,79.6072,11.35,0,0,0.95,1
w100_normal_r2,0,2,84,7891.8,10.6844,8.8024,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,4.6,入口で待った,103.0234,109.9807,103.0234,0,0,4.6,0,0
w100_normal_r2,0,1,85,7886.9,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,87.0731,71.928,71.928,0,0,0,0.95,0
w100_normal_r2,0,1,85,7890.35,10.9443,12.4569,MainAisle,スタッフとすれ違った,Staff01,0,1.65,自分で動き出した,182.5647,97.6461,97.6461,0.7,0,0,0.95,0
w100_normal_r2,0,1,85,7898.85,7.4818,11.3329,ColumnAisle02_N,椅子・荷物・ベビーカー,Table35/Chair,0,51.8995,経路を引き直した,0,275,0,50.9495,0,0,0.95,5
w100_normal_r2,0,1,85,7958.8,10.1999,10.0862,ColumnAisle03_N,もう1台のロボット,ServingRobot02,0,21.3,経路を引き直した,150.0512,105.0221,105.0221,20.35,0,0,0.95,2
w100_normal_r2,0,1,85,7983.35,10.1284,11.6723,NorthAisle,椅子・荷物・ベビーカー,Table14/Chair,0,60.0494,動けなくなった,66.3201,188.7135,66.3201,60.0494,0,0,0,5
w100_normal_r2,0,2,86,7896.45,11.15,13.3,MainAisle,分からない,,0,0.95,自分で動き出した,55.9243,55.9244,55.9243,0,0,0,0.95,0
w100_normal_r2,0,2,86,7905.5,10.7378,8.9673,MainAisle,人が立ち止まっていた,Staff01,0,5.25,自分で動き出した,97.4271,115.1548,97.4271,4.3,0,0,0.95,0
w100_normal_r2,0,2,86,7911.1,10.7322,8.9277,MainAisle,通路の順番待ち,先に入っている台:ServingRobot01,0,39.9997,入口で待った,97.9922,114.5898,97.9922,0,0,39.9997,0,0
w100_normal_r2,0,2,86,7952.25,10.6683,9.282,MainAisle,人が立ち止まっていた,Staff02,0,29.4998,経路を引き直した,108.9538,105.2447,105.2447,27.5999,0,0,1.9,2
w100_normal_r2,0,2,86,7984.85,10.3966,10.7877,MainAisle,もう1台のロボット,ServingRobot01,0,60.0494,動けなくなった,81.3467,132.8515,81.3467,60.0494,0,0,0,5
groups.csv
condition_id,seed,group_id,size,table_id,table_seats,arrive_s,guide_start_s,seated_s,eat_end_s,pay_start_s,pay_s,left_s,waited_min,warmup,state
w100_normal_r2,0,1,3,0,0,0,-1,0,2412.926,2412.948,-1,3013.584,0,1,Done
w100_normal_r2,0,2,3,0,0,0,-1,0,2825.209,3190.557,-1,3791.193,0,1,Done
w100_normal_r2,0,3,3,0,0,0,-1,0,3107.666,3107.676,-1,3708.312,0,1,Done
w100_normal_r2,0,4,2,0,0,0,-1,0,2727.302,2727.305,-1,3327.941,0,1,Done
w100_normal_r2,0,5,1,5,4,0,-1,0,-1,-1,-1,-1,0,1,Seated
w100_normal_r2,0,6,1,0,0,0,-1,0,2777.043,2824.2,-1,3424.836,0,1,Done
w100_normal_r2,0,7,4,0,0,0,-1,0,2043.984,2043.989,-1,2644.625,0,1,Done
w100_normal_r2,0,8,4,0,0,0,-1,0,2329.125,2362.599,-1,2963.235,0,1,Done
w100_normal_r2,0,9,2,0,0,0,-1,0,2688.403,3132,-1,3732.636,0,1,Done
w100_normal_r2,0,10,2,0,0,0,-1,0,2619.505,2619.55,-1,3220.186,0,1,Done
w100_normal_r2,0,11,2,0,0,0,-1,0,3043.691,3479.489,-1,4080.125,0,1,Done
w100_normal_r2,0,12,1,0,0,0,-1,0,2313.714,2423.359,-1,3023.995,0,1,Done
w100_normal_r2,0,13,2,0,0,0,-1,0,2312.699,2312.701,-1,2913.337,0,1,Done
w100_normal_r2,0,14,2,0,0,0,-1,0,1578.868,1578.885,-1,2179.521,0,1,Done
w100_normal_r2,0,15,1,0,0,0,-1,0,2502.036,2502.085,-1,3102.721,0,1,Done
w100_normal_r2,0,16,2,0,0,0,-1,0,1663.036,2042.737,-1,2643.373,0,1,Done
w100_normal_r2,0,17,2,0,0,0,-1,0,1696.565,1696.6,-1,2297.236,0,1,Done
w100_normal_r2,0,18,3,0,0,0,-1,0,1606.517,1606.562,-1,2207.198,0,1,Done
w100_normal_r2,0,19,4,0,0,0,-1,0,1256.962,1256.971,-1,1857.607,0,1,Done
w100_normal_r2,0,20,4,0,0,0,-1,0,989.3366,989.3519,1488.797,1488.797,0,1,Done
w100_normal_r2,0,21,2,21,4,0,-1,0,977.5092,-1,-1,-1,0,1,Eating
w100_normal_r2,0,22,3,0,0,0,-1,0,852.7088,885.4773,1135.002,1135.002,0,1,Done
w100_normal_r2,0,23,2,0,0,0,-1,0,553.342,1070.289,1495.754,1495.754,0,1,Done
w100_normal_r2,0,24,4,0,0,0,-1,0,785.4813,785.5017,1072.741,1072.741,0,1,Done
w100_normal_r2,0,25,4,0,0,0,-1,0,397.1041,922.6682,1405.566,1405.566,0,1,Done
w100_normal_r2,0,26,4,0,0,0,-1,0,404.3401,404.3448,686.9758,686.9758,0,1,Done
w100_normal_r2,0,27,4,0,0,0,-1,0,253.3027,400.4957,671.8295,671.8295,0,1,Done
w100_normal_r2,0,28,4,0,0,0,-1,0,62.4598,112.7723,452.783,452.783,0,1,Done
w100_normal_r2,0,29,3,29,4,67.7195,67.7195,182.8766,-1,-1,-1,-1,1.9193,0,Seated
w100_normal_r2,0,30,2,30,4,102.6217,102.6217,464.0302,-1,-1,-1,-1,6.0235,0,Seated
w100_normal_r2,0,31,2,31,4,225.2291,225.2291,514.118,-1,-1,-1,-1,4.8148,0,Seated
w100_normal_r2,0,32,4,32,4,232.5296,232.5296,536.5625,-1,-1,-1,-1,5.0672,0,Seated
w100_normal_r2,0,33,2,0,0,580.5518,580.5518,787.8011,3974.377,4550.01,-1,5147.716,3.4542,0,Done
w100_normal_r2,0,34,2,0,0,587.9999,587.9999,805.2469,3364.254,4136.482,-1,4734.188,3.6208,0,Done
w100_normal_r2,0,35,4,0,0,704.3215,704.3215,969.8567,4236.207,4484.417,-1,5082.124,4.4256,0,Done
w100_normal_r2,0,36,2,0,0,901.8733,1052.822,1371.182,5115.257,5115.293,-1,5713,7.8218,0,Done
w100_normal_r2,0,37,3,0,0,1003.099,1272.135,1744.546,5076.159,5110.064,-1,5707.77,12.3575,0,Done
w100_normal_r2,0,38,4,0,0,1046.015,1287.3,1813.514,5415.241,5415.267,-1,6012.973,12.7917,0,Done
w100_normal_r2,0,39,4,0,0,1056.325,1673.477,2307.446,5378.621,5821.375,-1,6419.081,20.852,0,Done
w100_normal_r2,0,40,2,0,0,1072.14,1735.738,2411.397,5937.849,5937.868,-1,6535.574,22.3209,0,Done
w100_normal_r2,0,41,1,0,0,1288.351,1857.657,2484.819,5911.526,5911.571,-1,6509.277,19.9411,0,Done
w100_normal_r2,0,42,4,0,0,1450.459,2006.302,2653.934,6459.394,6459.422,-1,7057.128,20.0579,0,Done
w100_normal_r2,0,43,1,0,0,1474.933,2089.533,2742.97,6240.855,6784.896,-1,7382.602,21.134,0,Done
w100_normal_r2,0,44,2,0,0,1500.759,2096.49,2726.004,6347.858,7113.159,-1,7710.865,20.4208,0,Done
w100_normal_r2,0,45,1,0,0,1593.799,2179.571,2987.209,6100.331,6100.331,-1,6698.037,23.2235,0,Done
w100_normal_r2,0,46,2,0,0,1790.191,2207.248,2955.928,5938.168,5938.216,-1,6535.922,19.429,0,Done
w100_normal_r2,0,47,2,0,0,2147.389,2297.286,3056.877,6326.362,6326.394,-1,6924.1,15.1581,0,Done
w100_normal_r2,0,48,4,0,0,2187.879,2644.675,3321.735,6626.174,6626.218,-1,7223.924,18.8976,0,Done
w100_normal_r2,0,49,1,0,0,2367.754,2643.423,3272.237,6459.302,6459.323,-1,7057.029,15.0747,0,Done
w100_normal_r2,0,50,2,0,0,2402.588,2913.387,3655.361,6906.135,7457.259,-1,8054.965,20.8795,0,Done
w100_normal_r2,0,51,2,8,4,2463.348,2963.285,3709.914,7271.458,7898.877,-1,-1,20.7761,0,Paying
w100_normal_r2,0,52,2,1,4,2522.355,3013.635,3776.679,7777.398,7777.404,-1,-1,20.9054,0,Paying
w100_normal_r2,0,53,2,12,2,2638.619,3024.045,3962.56,8160.471,-1,-1,-1,22.0657,0,Eating
w100_normal_r2,0,54,1,15,2,2832.458,3102.771,3928.677,8022.18,8022.194,-1,-1,18.2703,0,Paying
w100_normal_r2,0,55,3,4,4,3039.159,3327.991,4307.013,8484.064,-1,-1,-1,21.1309,0,Eating
w100_normal_r2,0,56,4,6,4,3166.784,3424.886,4477.993,8686.146,-1,-1,-1,21.8535,0,Eating
w100_normal_r2,0,57,2,10,2,3299.914,3299.914,4223.839,8147.067,-1,-1,-1,15.3988,0,Eating
w100_normal_r2,0,58,4,3,4,3400.011,3708.362,4619.886,8653.971,-1,-1,-1,20.3313,0,Eating
w100_normal_r2,0,59,1,9,2,3432.343,3732.686,4694.543,8642.841,-1,-1,-1,21.0367,0,Eating
w100_normal_r2,0,60,4,2,4,3611.318,3791.243,4784.441,9307.662,-1,-1,-1,19.5521,0,Eating
w100_normal_r2,0,61,3,34,4,3644.65,4734.238,6072.291,9927.591,-1,-1,-1,40.4607,0,Eating
w100_normal_r2,0,62,1,11,2,3646.802,4080.175,5180.289,9290.399,-1,-1,-1,25.5581,0,Eating
w100_normal_r2,0,63,2,35,4,3763.166,5082.173,6230.271,-1,-1,-1,-1,41.1184,0,Seated
w100_normal_r2,0,64,2,33,4,3792.594,5147.766,6422.218,10156.18,-1,-1,-1,43.8271,0,Eating
w100_normal_r2,0,65,2,27,4,3999.596,5707.82,6930.923,-1,-1,-1,-1,48.8555,0,Seated
w100_normal_r2,0,66,4,28,4,4068.864,5713.049,6995.968,-1,-1,-1,-1,48.7851,0,Seated
w100_normal_r2,0,67,1,26,4,4201.328,6013.023,7069.181,-1,-1,-1,-1,47.7976,0,Seated
w100_normal_r2,0,68,4,24,4,4216.767,6419.13,7303.014,-1,-1,-1,-1,51.4375,0,Seated
w100_normal_r2,0,69,2,19,4,4218.809,6509.327,7367.611,-1,-1,-1,-1,52.48,0,Seated
w100_normal_r2,0,70,4,22,4,4266.622,6535.624,7439.031,-1,-1,-1,-1,52.8735,0,Seated
w100_normal_r2,0,71,3,18,4,4269.61,6535.972,7507.512,-1,-1,-1,-1,53.965,0,Seated
w100_normal_r2,0,72,1,14,2,4281.912,6698.086,7761.815,-1,-1,-1,-1,57.9984,0,Seated
w100_normal_r2,0,73,1,17,2,4395.616,6924.15,7820.634,-1,-1,-1,-1,57.0836,0,Seated
w100_normal_r2,0,74,2,16,2,4520.775,7057.079,8032.005,-1,-1,-1,-1,58.5205,0,Seated
w100_normal_r2,0,75,2,25,4,4524.311,7057.178,8099.092,-1,-1,-1,-1,59.5797,0,Seated
w100_normal_r2,0,76,4,7,4,4597.275,7223.974,-1,-1,-1,-1,-1,-1,0,Guided
w100_normal_r2,0,77,3,20,4,4778.962,7382.652,-1,-1,-1,-1,-1,-1,0,Guided
w100_normal_r2,0,78,2,23,4,4844.356,7710.915,-1,-1,-1,-1,-1,-1,0,Guided
w100_normal_r2,0,79,4,0,0,4913.485,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,80,4,0,0,4942.87,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,81,2,13,2,5022.557,8055.015,-1,-1,-1,-1,-1,-1,0,Guided
w100_normal_r2,0,82,1,0,0,5047.21,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,83,2,0,0,5086.556,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,84,2,0,0,5159.072,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,85,4,0,0,5173.416,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,86,2,0,0,5238.809,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,87,2,0,0,5242.993,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,88,4,0,0,5347.035,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,89,2,0,0,5426.772,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,90,2,0,0,5464.375,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,91,2,0,0,5556.663,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,92,2,0,0,5629.875,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,93,2,0,0,5652.686,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,94,4,0,0,5878.75,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,95,2,0,0,6062.778,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,96,4,0,0,6099.235,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,97,3,0,0,6191.324,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,98,2,0,0,6194.262,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,99,3,0,0,6300.247,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,100,3,0,0,6356.227,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,101,3,0,0,6427.448,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,102,3,0,0,6467.391,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,103,2,0,0,6546.979,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,104,2,0,0,6619.246,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,105,3,0,0,6845.11,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,106,2,0,0,7103.098,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,107,1,0,0,7183.035,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,108,3,0,0,7184.18,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,109,2,0,0,7322.189,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,110,4,0,0,7537.196,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,111,2,0,0,7647.314,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,112,4,0,0,7870.439,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,113,1,0,0,7957.498,-1,-1,-1,-1,-1,-1,-1,0,Waiting
w100_normal_r2,0,114,4,0,0,8004.812,-1,-1,-1,-1,-1,-1,-1,0,Waiting
dishes.csv
condition_id,seed,t_s,dish_id,table_id,robot_id,fragile,ready_s,waited_s,requeued_so_far,why
w100_normal_r2,0,305.9188,42,4,1,0,127.0232,178.8957,1,動けなくなった
w100_normal_r2,0,305.9188,41,3,1,0,98.9214,206.9974,1,動けなくなった
w100_normal_r2,0,396.4467,45,8,1,0,262.6794,133.7673,1,動けなくなった
w100_normal_r2,0,396.4467,42,4,1,0,127.0232,269.4236,2,動けなくなった
w100_normal_r2,0,396.4467,41,3,1,0,98.9214,297.5253,2,動けなくなった
w100_normal_r2,0,396.4467,44,7,2,0,226.1292,170.3175,1,動けなくなった
w100_normal_r2,0,660.7322,50,12,1,0,418.4413,242.2908,1,動けなくなった
w100_normal_r2,0,660.7322,48,25,1,1,372.6025,288.1296,1,動けなくなった
w100_normal_r2,0,660.7322,49,5,1,0,408.6437,252.0884,1,動けなくなった
w100_normal_r2,0,780.8029,48,25,1,1,372.6025,408.2003,2,動けなくなった
w100_normal_r2,0,780.8029,49,5,1,0,408.6437,372.1591,2,動けなくなった
w100_normal_r2,0,780.8528,51,11,2,0,548.3096,232.5432,1,動けなくなった
w100_normal_r2,0,780.8528,52,10,2,0,563.306,217.5469,1,動けなくなった
w100_normal_r2,0,780.8528,53,9,2,0,573.8034,207.0494,1,動けなくなった
w100_normal_r2,0,1070.239,59,5,2,0,689.4752,380.7634,1,動けなくなった
w100_normal_r2,0,1148.165,62,31,1,0,1009.047,139.1174,1,動けなくなった
w100_normal_r2,0,1233.047,62,31,1,0,1009.047,224.0002,2,動けなくなった
w100_normal_r2,0,1233.047,65,29,1,0,1065.884,167.1631,1,動けなくなった
w100_normal_r2,0,1233.047,66,21,1,1,1089.207,143.8403,1,動けなくなった
w100_normal_r2,0,1319.081,62,31,1,0,1009.047,310.0342,3,動けなくなった
w100_normal_r2,0,1319.081,65,29,1,0,1065.884,253.197,2,動けなくなった
w100_normal_r2,0,1319.081,66,21,1,1,1089.207,229.8743,2,動けなくなった
w100_normal_r2,0,1446.005,62,31,1,0,1009.047,436.958,4,動けなくなった
w100_normal_r2,0,1446.005,65,29,1,0,1065.884,380.1208,3,動けなくなった
w100_normal_r2,0,1446.005,66,21,1,1,1089.207,356.7981,3,動けなくなった
w100_normal_r2,0,1530.888,62,31,1,0,1009.047,521.8408,5,動けなくなった
w100_normal_r2,0,1530.888,65,29,1,0,1065.884,465.0037,4,動けなくなった
w100_normal_r2,0,1530.888,66,21,1,1,1089.207,441.6809,4,動けなくなった
w100_normal_r2,0,1615.771,62,31,1,0,1009.047,606.7236,6,動けなくなった
w100_normal_r2,0,1615.771,65,29,1,0,1065.884,549.8865,5,動けなくなった
w100_normal_r2,0,1615.771,66,21,1,1,1089.207,526.5637,5,動けなくなった
w100_normal_r2,0,1670.925,64,32,2,0,1054.573,616.3513,1,打ち切り
w100_normal_r2,0,1670.925,63,30,2,0,1051.02,619.9048,1,打ち切り
w100_normal_r2,0,1670.925,59,5,2,0,689.4752,981.4494,2,打ち切り
w100_normal_r2,0,1700.654,62,31,1,0,1009.047,691.6064,7,動けなくなった
w100_normal_r2,0,1700.654,65,29,1,0,1065.884,634.7693,6,動けなくなった
w100_normal_r2,0,1700.654,66,21,1,1,1089.207,611.4465,6,動けなくなった
w100_normal_r2,0,1787.889,62,31,1,0,1009.047,778.8416,8,動けなくなった
w100_normal_r2,0,1787.889,65,29,1,0,1065.884,722.0044,7,動けなくなった
w100_normal_r2,0,1787.889,66,21,1,1,1089.207,698.6816,7,動けなくなった
w100_normal_r2,0,1872.771,62,31,1,0,1009.047,863.7244,9,動けなくなった
w100_normal_r2,0,1872.771,65,29,1,0,1065.884,806.8872,8,動けなくなった
w100_normal_r2,0,1872.771,66,21,1,1,1089.207,783.5645,8,動けなくなった
w100_normal_r2,0,1957.404,62,31,1,0,1009.047,948.3569,10,動けなくなった
w100_normal_r2,0,1957.404,65,29,1,0,1065.884,891.5198,9,動けなくなった
w100_normal_r2,0,1957.404,66,21,1,1,1089.207,868.197,9,動けなくなった
w100_normal_r2,0,2042.287,62,31,1,0,1009.047,1033.24,11,動けなくなった
w100_normal_r2,0,2042.287,65,29,1,0,1065.884,976.4026,10,動けなくなった
w100_normal_r2,0,2042.287,66,21,1,1,1089.207,953.0798,10,動けなくなった
w100_normal_r2,0,2127.17,62,31,1,0,1009.047,1118.123,12,動けなくなった
w100_normal_r2,0,2127.17,65,29,1,0,1065.884,1061.285,11,動けなくなった
w100_normal_r2,0,2127.17,66,21,1,1,1089.207,1037.963,11,動けなくなった
w100_normal_r2,0,2270.51,62,31,1,0,1009.047,1261.462,13,動けなくなった
w100_normal_r2,0,2270.51,65,29,1,0,1065.884,1204.625,12,動けなくなった
w100_normal_r2,0,2270.51,66,21,1,1,1089.207,1181.302,12,動けなくなった
w100_normal_r2,0,2271.611,64,32,2,0,1054.573,1217.037,2,打ち切り
w100_normal_r2,0,2271.611,63,30,2,0,1051.02,1220.591,2,打ち切り
w100_normal_r2,0,2271.611,59,5,2,0,689.4752,1582.135,3,打ち切り
w100_normal_r2,0,2355.843,62,31,1,0,1009.047,1346.796,14,動けなくなった
w100_normal_r2,0,2355.843,65,29,1,0,1065.884,1289.958,13,動けなくなった
w100_normal_r2,0,2355.843,66,21,1,1,1089.207,1266.636,13,動けなくなった
w100_normal_r2,0,2461.196,62,31,1,0,1009.047,1452.148,15,動けなくなった
w100_normal_r2,0,2461.196,65,29,1,0,1065.884,1395.311,14,動けなくなった
w100_normal_r2,0,2461.196,66,21,1,1,1089.207,1371.989,14,動けなくなった
w100_normal_r2,0,2547.48,62,31,1,0,1009.047,1538.433,16,動けなくなった
w100_normal_r2,0,2547.48,65,29,1,0,1065.884,1481.595,15,動けなくなった
w100_normal_r2,0,2547.48,66,21,1,1,1089.207,1458.273,15,動けなくなった
w100_normal_r2,0,2639.87,62,31,1,0,1009.047,1630.823,17,動けなくなった
w100_normal_r2,0,2639.87,65,29,1,0,1065.884,1573.986,16,動けなくなった
w100_normal_r2,0,2639.87,66,21,1,1,1089.207,1550.663,16,動けなくなった
w100_normal_r2,0,2772.099,62,31,1,0,1009.047,1763.052,18,動けなくなった
w100_normal_r2,0,2772.099,65,29,1,0,1065.884,1706.215,17,動けなくなった
w100_normal_r2,0,2772.099,66,21,1,1,1089.207,1682.892,17,動けなくなった
w100_normal_r2,0,2872.297,64,32,2,0,1054.573,1817.723,3,打ち切り
w100_normal_r2,0,2872.297,63,30,2,0,1051.02,1821.277,3,打ち切り
w100_normal_r2,0,2872.297,59,5,2,0,689.4752,2182.822,4,打ち切り
w100_normal_r2,0,2876.501,62,31,1,0,1009.047,1867.454,19,動けなくなった
w100_normal_r2,0,2876.501,65,29,1,0,1065.884,1810.616,18,動けなくなった
w100_normal_r2,0,2876.501,66,21,1,1,1089.207,1787.294,18,動けなくなった
w100_normal_r2,0,2969.542,62,31,1,0,1009.047,1960.494,20,動けなくなった
w100_normal_r2,0,2969.542,65,29,1,0,1065.884,1903.657,19,動けなくなった
w100_normal_r2,0,2969.542,66,21,1,1,1089.207,1880.334,19,動けなくなった
w100_normal_r2,0,3099.568,62,31,1,0,1009.047,2090.521,21,動けなくなった
w100_normal_r2,0,3099.568,65,29,1,0,1065.884,2033.684,20,動けなくなった
w100_normal_r2,0,3099.568,66,21,1,1,1089.207,2010.361,20,動けなくなった
w100_normal_r2,0,3266.281,64,32,2,0,1054.573,2211.708,4,動けなくなった
w100_normal_r2,0,3266.281,63,30,2,0,1051.02,2215.261,4,動けなくなった
w100_normal_r2,0,3266.281,59,5,2,0,689.4752,2576.806,5,動けなくなった
w100_normal_r2,0,3311.825,62,31,1,0,1009.047,2302.778,22,動けなくなった
w100_normal_r2,0,3311.825,65,29,1,0,1065.884,2245.941,21,動けなくなった
w100_normal_r2,0,3311.825,66,21,1,1,1089.207,2222.618,21,動けなくなった
w100_normal_r2,0,3386.949,62,31,1,0,1009.047,2377.902,23,動けなくなった
w100_normal_r2,0,3386.949,65,29,1,0,1065.884,2321.064,22,動けなくなった
w100_normal_r2,0,3386.949,66,21,1,1,1089.207,2297.742,22,動けなくなった
w100_normal_r2,0,3462.072,62,31,1,0,1009.047,2453.025,24,動けなくなった
w100_normal_r2,0,3462.072,65,29,1,0,1065.884,2396.188,23,動けなくなった
w100_normal_r2,0,3462.072,66,21,1,1,1089.207,2372.865,23,動けなくなった
w100_normal_r2,0,3622.278,62,31,1,0,1009.047,2613.231,25,動けなくなった
w100_normal_r2,0,3622.278,65,29,1,0,1065.884,2556.394,24,動けなくなった
w100_normal_r2,0,3622.278,66,21,1,1,1089.207,2533.071,24,動けなくなった
w100_normal_r2,0,3689.744,64,32,2,0,1054.573,2635.171,5,動けなくなった
w100_normal_r2,0,3689.744,63,30,2,0,1051.02,2638.724,5,動けなくなった
w100_normal_r2,0,3689.744,59,5,2,0,689.4752,3000.269,6,動けなくなった
w100_normal_r2,0,3772.275,62,31,1,0,1009.047,2763.228,26,動けなくなった
w100_normal_r2,0,3772.275,65,29,1,0,1065.884,2706.39,25,動けなくなった
w100_normal_r2,0,3772.275,66,21,1,1,1089.207,2683.068,25,動けなくなった
w100_normal_r2,0,3857.157,62,31,1,0,1009.047,2848.11,27,動けなくなった
w100_normal_r2,0,3857.157,65,29,1,0,1065.884,2791.273,26,動けなくなった
w100_normal_r2,0,3857.157,66,21,1,1,1089.207,2767.95,26,動けなくなった
w100_normal_r2,0,3942.291,62,31,1,0,1009.047,2933.243,28,動けなくなった
w100_normal_r2,0,3942.291,65,29,1,0,1065.884,2876.406,27,動けなくなった
w100_normal_r2,0,3942.291,66,21,1,1,1089.207,2853.083,27,動けなくなった
w100_normal_r2,0,4119.15,62,31,1,0,1009.047,3110.103,29,動けなくなった
w100_normal_r2,0,4119.15,65,29,1,0,1065.884,3053.266,28,動けなくなった
w100_normal_r2,0,4119.15,66,21,1,1,1089.207,3029.943,28,動けなくなった
w100_normal_r2,0,4203.967,62,31,1,0,1009.047,3194.92,30,動けなくなった
w100_normal_r2,0,4203.967,65,29,1,0,1065.884,3138.083,29,動けなくなった
w100_normal_r2,0,4203.967,66,21,1,1,1089.207,3114.76,29,動けなくなった
w100_normal_r2,0,4289.482,64,32,2,0,1054.573,3234.909,6,打ち切り
w100_normal_r2,0,4289.482,63,30,2,0,1051.02,3238.462,6,打ち切り
w100_normal_r2,0,4289.482,59,5,2,0,689.4752,3600.007,7,打ち切り
w100_normal_r2,0,4312.741,62,31,1,0,1009.047,3303.694,31,動けなくなった
w100_normal_r2,0,4312.741,65,29,1,0,1065.884,3246.856,30,動けなくなった
w100_normal_r2,0,4312.741,66,21,1,1,1089.207,3223.534,30,動けなくなった
w100_normal_r2,0,4397.209,62,31,1,0,1009.047,3388.162,32,動けなくなった
w100_normal_r2,0,4397.209,65,29,1,0,1065.884,3331.325,31,動けなくなった
w100_normal_r2,0,4397.209,66,21,1,1,1089.207,3308.002,31,動けなくなった
w100_normal_r2,0,4482.027,62,31,1,0,1009.047,3472.98,33,動けなくなった
w100_normal_r2,0,4482.027,65,29,1,0,1065.884,3416.143,32,動けなくなった
w100_normal_r2,0,4482.027,66,21,1,1,1089.207,3392.82,32,動けなくなった
w100_normal_r2,0,4566.496,62,31,1,0,1009.047,3557.448,34,動けなくなった
w100_normal_r2,0,4566.496,65,29,1,0,1065.884,3500.611,33,動けなくなった
w100_normal_r2,0,4566.496,66,21,1,1,1089.207,3477.289,33,動けなくなった
w100_normal_r2,0,4650.964,62,31,1,0,1009.047,3641.917,35,動けなくなった
w100_normal_r2,0,4650.964,65,29,1,0,1065.884,3585.08,34,動けなくなった
w100_normal_r2,0,4650.964,66,21,1,1,1089.207,3561.757,34,動けなくなった
w100_normal_r2,0,4736.877,62,31,1,0,1009.047,3727.83,36,動けなくなった
w100_normal_r2,0,4736.877,65,29,1,0,1065.884,3670.993,35,動けなくなった
w100_normal_r2,0,4736.877,66,21,1,1,1089.207,3647.67,35,動けなくなった
w100_normal_r2,0,4821.346,62,31,1,0,1009.047,3812.299,37,動けなくなった
w100_normal_r2,0,4821.346,65,29,1,0,1065.884,3755.462,36,動けなくなった
w100_normal_r2,0,4821.346,66,21,1,1,1089.207,3732.139,36,動けなくなった
w100_normal_r2,0,4887.238,64,32,2,0,1054.573,3832.665,7,打ち切り
w100_normal_r2,0,4887.238,63,30,2,0,1051.02,3836.218,7,打ち切り
w100_normal_r2,0,4887.238,59,5,2,0,689.4752,4197.763,8,打ち切り
w100_normal_r2,0,4905.815,62,31,1,0,1009.047,3896.768,38,動けなくなった
w100_normal_r2,0,4905.815,65,29,1,0,1065.884,3839.931,37,動けなくなった
w100_normal_r2,0,4905.815,66,21,1,1,1089.207,3816.608,37,動けなくなった
w100_normal_r2,0,4992.226,62,31,1,0,1009.047,3983.179,39,動けなくなった
w100_normal_r2,0,4992.226,65,29,1,0,1065.884,3926.342,38,動けなくなった
w100_normal_r2,0,4992.226,66,21,1,1,1089.207,3903.019,38,動けなくなった
w100_normal_r2,0,5078.637,62,31,1,0,1009.047,4069.59,40,動けなくなった
w100_normal_r2,0,5078.637,65,29,1,0,1065.884,4012.753,39,動けなくなった
w100_normal_r2,0,5078.637,66,21,1,1,1089.207,3989.43,39,動けなくなった
w100_normal_r2,0,5163.106,62,31,1,0,1009.047,4154.059,41,動けなくなった
w100_normal_r2,0,5163.106,65,29,1,0,1065.884,4097.222,40,動けなくなった
w100_normal_r2,0,5163.106,66,21,1,1,1089.207,4073.899,40,動けなくなった
w100_normal_r2,0,5247.575,62,31,1,0,1009.047,4238.527,42,動けなくなった
w100_normal_r2,0,5247.575,65,29,1,0,1065.884,4181.69,41,動けなくなった
w100_normal_r2,0,5247.575,66,21,1,1,1089.207,4158.368,41,動けなくなった
w100_normal_r2,0,5339.116,62,31,1,0,1009.047,4330.068,43,動けなくなった
w100_normal_r2,0,5339.116,65,29,1,0,1065.884,4273.231,42,動けなくなった
w100_normal_r2,0,5339.116,66,21,1,1,1089.207,4249.909,42,動けなくなった
w100_normal_r2,0,5425.776,62,31,1,0,1009.047,4416.729,44,動けなくなった
w100_normal_r2,0,5425.776,65,29,1,0,1065.884,4359.892,43,動けなくなった
w100_normal_r2,0,5425.776,66,21,1,1,1089.207,4336.569,43,動けなくなった
w100_normal_r2,0,5484.994,64,32,2,0,1054.573,4430.42,8,打ち切り
w100_normal_r2,0,5484.994,63,30,2,0,1051.02,4433.974,8,打ち切り
w100_normal_r2,0,5484.994,59,5,2,0,689.4752,4795.519,9,打ち切り
w100_normal_r2,0,5522.297,62,31,1,0,1009.047,4513.25,45,動けなくなった
w100_normal_r2,0,5522.297,65,29,1,0,1065.884,4456.413,44,動けなくなった
w100_normal_r2,0,5522.297,66,21,1,1,1089.207,4433.09,44,動けなくなった
w100_normal_r2,0,5610.601,62,31,1,0,1009.047,4601.554,46,動けなくなった
w100_normal_r2,0,5610.601,65,29,1,0,1065.884,4544.717,45,動けなくなった
w100_normal_r2,0,5610.601,66,21,1,1,1089.207,4521.394,45,動けなくなった
w100_normal_r2,0,5695.07,62,31,1,0,1009.047,4686.022,47,動けなくなった
w100_normal_r2,0,5695.07,65,29,1,0,1065.884,4629.186,46,動けなくなった
w100_normal_r2,0,5695.07,66,21,1,1,1089.207,4605.863,46,動けなくなった
w100_normal_r2,0,5779.539,62,31,1,0,1009.047,4770.491,48,動けなくなった
w100_normal_r2,0,5779.539,65,29,1,0,1065.884,4713.654,47,動けなくなった
w100_normal_r2,0,5779.539,66,21,1,1,1089.207,4690.332,47,動けなくなった
w100_normal_r2,0,5864.007,62,31,1,0,1009.047,4854.96,49,動けなくなった
w100_normal_r2,0,5864.007,65,29,1,0,1065.884,4798.123,48,動けなくなった
w100_normal_r2,0,5864.007,66,21,1,1,1089.207,4774.8,48,動けなくなった
w100_normal_r2,0,6001.418,62,31,1,0,1009.047,4992.371,50,動けなくなった
w100_normal_r2,0,6001.418,65,29,1,0,1065.884,4935.534,49,動けなくなった
w100_normal_r2,0,6001.418,66,21,1,1,1089.207,4912.211,49,動けなくなった
w100_normal_r2,0,6082.75,64,32,2,0,1054.573,5028.176,9,打ち切り
w100_normal_r2,0,6082.75,63,30,2,0,1051.02,5031.729,9,打ち切り
w100_normal_r2,0,6082.75,59,5,2,0,689.4752,5393.274,10,打ち切り
w100_normal_r2,0,6093.806,62,31,1,0,1009.047,5084.759,51,動けなくなった
w100_normal_r2,0,6093.806,65,29,1,0,1065.884,5027.922,50,動けなくなった
w100_normal_r2,0,6093.806,66,21,1,1,1089.207,5004.599,50,動けなくなった
w100_normal_r2,0,6178.275,62,31,1,0,1009.047,5169.228,52,動けなくなった
w100_normal_r2,0,6178.275,65,29,1,0,1065.884,5112.391,51,動けなくなった
w100_normal_r2,0,6178.275,66,21,1,1,1089.207,5089.068,51,動けなくなった
w100_normal_r2,0,6342.033,62,31,1,0,1009.047,5332.985,53,動けなくなった
w100_normal_r2,0,6342.033,65,29,1,0,1065.884,5276.148,52,動けなくなった
w100_normal_r2,0,6342.033,66,21,1,1,1089.207,5252.826,52,動けなくなった
w100_normal_r2,0,6365.441,64,32,2,0,1054.573,5310.868,10,動けなくなった
w100_normal_r2,0,6365.441,63,30,2,0,1051.02,5314.421,10,動けなくなった
w100_normal_r2,0,6365.441,59,5,2,0,689.4752,5675.966,11,動けなくなった
w100_normal_r2,0,6534.827,64,32,2,0,1054.573,5480.253,11,動けなくなった
w100_normal_r2,0,6534.827,63,30,2,0,1051.02,5483.807,11,動けなくなった
w100_normal_r2,0,6534.827,59,5,2,0,689.4752,5845.352,12,動けなくなった
w100_normal_r2,0,6540.803,62,31,1,0,1009.047,5531.756,54,動けなくなった
w100_normal_r2,0,6540.803,65,29,1,0,1065.884,5474.919,53,動けなくなった
w100_normal_r2,0,6540.803,66,21,1,1,1089.207,5451.596,53,動けなくなった
w100_normal_r2,0,6611.576,64,32,2,0,1054.573,5557.002,12,動けなくなった
w100_normal_r2,0,6611.576,63,30,2,0,1051.02,5560.556,12,動けなくなった
w100_normal_r2,0,6611.576,59,5,2,0,689.4752,5922.101,13,動けなくなった
w100_normal_r2,0,6615.56,62,31,1,0,1009.047,5606.513,55,動けなくなった
w100_normal_r2,0,6615.56,65,29,1,0,1065.884,5549.676,54,動けなくなった
w100_normal_r2,0,6615.56,66,21,1,1,1089.207,5526.353,54,動けなくなった
w100_normal_r2,0,6688.873,64,32,2,0,1054.573,5634.299,13,動けなくなった
w100_normal_r2,0,6688.873,63,30,2,0,1051.02,5637.853,13,動けなくなった
w100_normal_r2,0,6688.873,59,5,2,0,689.4752,5999.397,14,動けなくなった
w100_normal_r2,0,6690.317,62,31,1,0,1009.047,5681.27,56,動けなくなった
w100_normal_r2,0,6690.317,65,29,1,0,1065.884,5624.433,55,動けなくなった
w100_normal_r2,0,6690.317,66,21,1,1,1089.207,5601.11,55,動けなくなった
w100_normal_r2,0,6824.69,62,31,1,0,1009.047,5815.643,57,動けなくなった
w100_normal_r2,0,6824.69,65,29,1,0,1065.884,5758.806,56,動けなくなった
w100_normal_r2,0,6824.69,66,21,1,1,1089.207,5735.483,56,動けなくなった
w100_normal_r2,0,6909.159,62,31,1,0,1009.047,5900.111,58,動けなくなった
w100_normal_r2,0,6909.159,65,29,1,0,1065.884,5843.274,57,動けなくなった
w100_normal_r2,0,6909.159,66,21,1,1,1089.207,5819.952,57,動けなくなった
w100_normal_r2,0,6993.627,62,31,1,0,1009.047,5984.58,59,動けなくなった
w100_normal_r2,0,6993.627,65,29,1,0,1065.884,5927.743,58,動けなくなった
w100_normal_r2,0,6993.627,66,21,1,1,1089.207,5904.42,58,動けなくなった
w100_normal_r2,0,7081.184,62,31,1,0,1009.047,6072.137,60,動けなくなった
w100_normal_r2,0,7081.184,65,29,1,0,1065.884,6015.3,59,動けなくなった
w100_normal_r2,0,7081.184,66,21,1,1,1089.207,5991.977,59,動けなくなった
w100_normal_r2,0,7165.653,62,31,1,0,1009.047,6156.605,61,動けなくなった
w100_normal_r2,0,7165.653,65,29,1,0,1065.884,6099.769,60,動けなくなった
w100_normal_r2,0,7165.653,66,21,1,1,1089.207,6076.446,60,動けなくなった
w100_normal_r2,0,7277.166,62,31,1,0,1009.047,6268.118,62,動けなくなった
w100_normal_r2,0,7277.166,65,29,1,0,1065.884,6211.281,61,動けなくなった
w100_normal_r2,0,7277.166,66,21,1,1,1089.207,6187.958,61,動けなくなった
w100_normal_r2,0,7286.628,64,32,2,0,1054.573,6232.055,14,打ち切り
w100_normal_r2,0,7286.628,63,30,2,0,1051.02,6235.608,14,打ち切り
w100_normal_r2,0,7286.628,59,5,2,0,689.4752,6597.153,15,打ち切り
w100_normal_r2,0,7361.634,62,31,1,0,1009.047,6352.587,63,動けなくなった
w100_normal_r2,0,7361.634,65,29,1,0,1065.884,6295.75,62,動けなくなった
w100_normal_r2,0,7361.634,66,21,1,1,1089.207,6272.427,62,動けなくなった
w100_normal_r2,0,7446.103,62,31,1,0,1009.047,6437.056,64,動けなくなった
w100_normal_r2,0,7446.103,65,29,1,0,1065.884,6380.219,63,動けなくなった
w100_normal_r2,0,7446.103,66,21,1,1,1089.207,6356.896,63,動けなくなった
w100_normal_r2,0,7543.87,62,31,1,0,1009.047,6534.822,65,動けなくなった
w100_normal_r2,0,7543.87,65,29,1,0,1065.884,6477.985,64,動けなくなった
w100_normal_r2,0,7543.87,66,21,1,1,1089.207,6454.663,64,動けなくなった
w100_normal_r2,0,7641.487,62,31,1,0,1009.047,6632.439,66,動けなくなった
w100_normal_r2,0,7641.487,65,29,1,0,1065.884,6575.603,65,動けなくなった
w100_normal_r2,0,7641.487,66,21,1,1,1089.207,6552.28,65,動けなくなった
w100_normal_r2,0,7725.956,62,31,1,0,1009.047,6716.908,67,動けなくなった
w100_normal_r2,0,7725.956,65,29,1,0,1065.884,6660.071,66,動けなくなった
w100_normal_r2,0,7725.956,66,21,1,1,1089.207,6636.749,66,動けなくなった
w100_normal_r2,0,7874.872,62,31,1,0,1009.047,6865.824,68,動けなくなった
w100_normal_r2,0,7874.872,65,29,1,0,1065.884,6808.987,67,動けなくなった
w100_normal_r2,0,7874.872,66,21,1,1,1089.207,6785.665,67,動けなくなった
w100_normal_r2,0,7884.384,64,32,2,0,1054.573,6829.811,15,打ち切り
w100_normal_r2,0,7884.384,63,30,2,0,1051.02,6833.364,15,打ち切り
w100_normal_r2,0,7884.384,59,5,2,0,689.4752,7194.909,16,打ち切り
w100_normal_r2,0,8045.702,62,31,1,0,1009.047,7036.654,69,動けなくなった
w100_normal_r2,0,8045.702,65,29,1,0,1065.884,6979.817,68,動けなくなった
w100_normal_r2,0,8045.702,66,21,1,1,1089.207,6956.495,68,動けなくなった
w100_normal_r2,0,8047.196,64,32,2,0,1054.573,6992.623,16,動けなくなった
w100_normal_r2,0,8047.196,63,30,2,0,1051.02,6996.176,16,動けなくなった
w100_normal_r2,0,8047.196,59,5,2,0,689.4752,7357.721,17,動けなくなった
give_ups.csv
condition_id,seed,t_s,who,job,table_id,to_x,to_z,x,z,aisle,waited_s,action
w100_normal_r2,0,60.3194,客,ドリンクバー,28,8.55,0.15,4.55,9.5,ColumnAisle01_N,0,あきらめた
w100_normal_r2,0,80.8703,客,ドリンクバー,27,8.55,0.15,4.55,8.2,ColumnAisle01_N,0,あきらめた
w100_normal_r2,0,81.5704,客,ドリンクバー,25,8.55,0.15,9.85,4.5,ColumnAisle03_S,0,あきらめた
w100_normal_r2,0,83.0205,客,ドリンクバー,26,8.55,0.15,9.85,5.8,ColumnAisle03_S,0,あきらめた
w100_normal_r2,0,84.6206,客,ドリンクバー,28,4.55,9.5,4.8,8.3246,ColumnAisle01_N,60,移した
w100_normal_r2,0,97.1213,客,ドリンクバー,28,4.55,9.5,4.8,8.2705,ColumnAisle01_N,60,移した
w100_normal_r2,0,109.9721,スタッフ,料理を運ぶ,3,1.9,4.625,4.5584,5.4525,ColumnAisle01_S,60.0494,あきらめた
w100_normal_r2,0,170.0758,スタッフ,料理を運ぶ,3,11.15,13.3,1.9866,5.2986,WestAisle_S,60.0494,あきらめた
w100_normal_r2,0,176.5262,客,ドリンクバー,27,4.55,8.2,5.25,6.6,CrossAisle,60,移した
w100_normal_r2,0,195.4773,客,ドリンクバー,27,8.1,0.15,7.744,1.5592,,60,あきらめた
w100_normal_r2,0,231.9296,客,ドリンクバー,26,9.85,5.8,11.6651,4.1978,MainAisle,60,移した
w100_normal_r2,0,253.1308,スタッフ,料理を運ぶ,7,1.9,12.025,6.15,12.2352,NorthAisle,60.0494,あきらめた
w100_normal_r2,0,266.0786,客,ドリンクバー,27,4.55,8.2,7.3546,7.8626,ColumnAisle02_N,60,移した
w100_normal_r2,0,366.8539,スタッフ,案内,30,7.2,8.2,10.25,9.5,ColumnAisle03_N,60.0494,あきらめた
w100_normal_r2,0,568.2548,客,ドリンクバー,14,10,11.9,7.4,5.7062,ColumnAisle02_S,60,移した
w100_normal_r2,0,737.2635,客,ドリンクバー,20,4.55,5.8,4.8,4.2144,ColumnAisle01_S,60,移した
w100_normal_r2,0,813.5948,客,ドリンクバー,7,1.9,12.025,5,12.8011,,60,移した
w100_normal_r2,0,855.6846,客,ドリンクバー,8,8.55,0.15,1.9,13.875,WestAisle_N,0,あきらめた
w100_normal_r2,0,964.308,客,ドリンクバー,19,4.55,4.5,7.35,4.3583,ColumnAisle02_S,60,移した
w100_normal_r2,0,988.3022,客,ドリンクバー,19,7.2,0.15,7.35,6.4939,CrossAisle,60,あきらめた
w100_normal_r2,0,1052.771,組,片付け,28,4.55,9.5,4.55,9.5,ColumnAisle01_N,600.027,卓を空けた
w100_normal_r2,0,1062.581,客,ドリンクバー,8,1.9,13.875,6.1257,7.35,CrossAisle,60,移した
w100_normal_r2,0,1107.975,客,ドリンクバー,11,5.65,11.9,6.9554,7.5699,CrossAisle,60,移した
w100_normal_r2,0,1132.9,スタッフ,呼び出し,13,8.55,11.9,4.6157,9.2073,ColumnAisle01_N,60.0494,あきらめた
w100_normal_r2,0,1193.058,スタッフ,呼び出し,13,8.55,11.9,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,1195.16,スタッフ,レジ,25,14.25,0.5,13.498,1.0616,,60.0494,あきらめた
w100_normal_r2,0,1255.319,スタッフ,レジ,25,14.25,0.5,13.4767,1.0454,,60.0494,あきらめた
w100_normal_r2,0,1272.085,組,片付け,27,4.55,8.2,4.55,8.2,ColumnAisle01_N,600.027,卓を空けた
w100_normal_r2,0,1287.25,組,片付け,26,9.85,5.8,9.85,5.8,ColumnAisle03_S,600.027,卓を空けた
w100_normal_r2,0,1294.057,客,ドリンクバー,12,7.1,11.9,7.35,6.3551,ColumnAisle02_S,60,移した
w100_normal_r2,0,1315.478,スタッフ,レジ,25,14.25,0.5,13.4998,1.057,,60.0494,移した
w100_normal_r2,0,1437.797,客,トイレ,12,7.1,11.9,8.4318,7.0374,CrossAisle,60,移した
w100_normal_r2,0,1486.244,客,ドリンクバー,13,8.55,11.9,10.2824,3.792,ColumnAisle03_S,60,移した
w100_normal_r2,0,1488.096,客,ドリンクバー,13,8.55,11.9,10.254,3.6108,ColumnAisle03_S,60,移した
w100_normal_r2,0,1584.941,スタッフ,料理を運ぶ,31,7.2,9.5,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,1673.427,組,片付け,24,9.85,3.2,9.85,3.2,ColumnAisle03_S,600.027,卓を空けた
w100_normal_r2,0,1689.693,スタッフ,料理を運ぶ,31,11.15,13.3,7.4,10.8832,ColumnAisle02_N,60.0494,あきらめた
w100_normal_r2,0,1735.688,組,片付け,22,7.2,4.5,7.2,4.5,ColumnAisle02_S,600.027,卓を空けた
w100_normal_r2,0,1838.438,スタッフ,料理を運ぶ,32,7.2,10.8,10.25,9.266,ColumnAisle03_N,60.0494,あきらめた
w100_normal_r2,0,1857.607,組,会計,19,4.55,4.5,4.55,4.5,ColumnAisle01_S,600.027,卓を空けた
w100_normal_r2,0,1938.786,スタッフ,呼び出し,11,5.65,11.9,9.101,12.4624,,60.0494,あきらめた
w100_normal_r2,0,1946.543,スタッフ,料理を運ぶ,32,11.15,13.3,7.35,11.0662,ColumnAisle02_N,60.0494,あきらめた
w100_normal_r2,0,2006.252,組,片付け,25,9.85,4.5,9.85,4.5,ColumnAisle03_S,600.027,卓を空けた
w100_normal_r2,0,2089.483,組,片付け,20,4.55,5.8,4.55,5.8,ColumnAisle01_S,600.027,卓を空けた
w100_normal_r2,0,2096.44,組,片付け,23,7.2,5.8,7.2,5.8,ColumnAisle02_S,600.027,卓を空けた
w100_normal_r2,0,2102.796,スタッフ,料理を運ぶ,16,11.15,13.3,7.35,10.8861,ColumnAisle02_N,60.0494,あきらめた
w100_normal_r2,0,2179.521,組,会計,14,10,11.9,10,11.9,NorthAisle,600.027,卓を空けた
w100_normal_r2,0,2207.198,組,会計,18,4.55,3.2,4.55,3.2,ColumnAisle01_S,600.027,卓を空けた
w100_normal_r2,0,2210.851,客,ドリンクバー,8,1.9,13.875,2.1178,11.1798,WestAisle_N,60,移した
w100_normal_r2,0,2276.615,客,ドリンクバー,33,9.85,8.2,10.25,4.5,ColumnAisle03_S,60,移した
w100_normal_r2,0,2297.236,組,会計,17,6.4,2.1,6.4,2.1,SouthAisle,600.027,卓を空けた
w100_normal_r2,0,2363.2,客,ドリンクバー,12,8.1,0.15,6.7092,11.9226,NorthAisle,60,あきらめた
w100_normal_r2,0,2423.309,客,ドリンクバー,12,7.1,11.9,7.95,12.55,,60,移した
w100_normal_r2,0,2426.712,スタッフ,料理を運ぶ,12,11.15,13.3,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,2541.874,スタッフ,料理を運ぶ,28,11.15,13.3,10.25,9.5,ColumnAisle03_N,60.0494,あきらめた
w100_normal_r2,0,2544.977,スタッフ,料理を運ぶ,3,11.15,13.3,7.35,10.4853,ColumnAisle02_N,60.0494,あきらめた
w100_normal_r2,0,2557.84,客,ドリンクバー,10,4.2,11.9,5.4913,7.2778,CrossAisle,60,移した
w100_normal_r2,0,2643.373,組,会計,16,4.575,2.1,4.575,2.1,SouthAisle,600.027,卓を空けた
w100_normal_r2,0,2644.625,組,会計,7,1.9,12.025,1.9,12.025,NorthAisle,600.027,卓を空けた
w100_normal_r2,0,2717.045,客,ドリンクバー,34,9.85,9.5,10.25,4.5,ColumnAisle03_S,60,移した
w100_normal_r2,0,2740.468,客,ドリンクバー,35,9.85,10.8,10.25,4.5,ColumnAisle03_S,60,移した
w100_normal_r2,0,2786.163,スタッフ,料理を運ぶ,6,11.15,13.3,7.4,10.7215,ColumnAisle02_N,60.0494,あきらめた
w100_normal_r2,0,2863.688,スタッフ,案内,14,10,11.9,11.7,8.6611,MainAisle,60.0494,あきらめた
w100_normal_r2,0,2884.258,スタッフ,料理を運ぶ,6,11.15,13.3,11.7,12.9457,MainAisle,60.0494,あきらめた
w100_normal_r2,0,2913.337,組,会計,13,8.55,11.9,8.55,11.9,NorthAisle,600.027,卓を空けた
w100_normal_r2,0,2923.847,スタッフ,案内,14,13.45,0.8,10.25,9.5,ColumnAisle03_N,60.0494,あきらめた
w100_normal_r2,0,2963.235,組,会計,8,1.9,13.875,1.9,13.875,WestAisle_N,600.027,卓を空けた
w100_normal_r2,0,3013.584,組,会計,1,1.9,0.925,1.9,0.925,WestAisle_S,600.027,卓を空けた
w100_normal_r2,0,3023.995,組,会計,12,7.1,11.9,7.1,11.9,NorthAisle,600.027,卓を空けた
w100_normal_r2,0,3071.391,スタッフ,料理を運ぶ,28,11.15,13.3,7.9501,12.55,,60.0494,あきらめた
w100_normal_r2,0,3102.721,組,会計,15,2.75,2.1,2.75,2.1,SouthAisle,600.027,卓を空けた
w100_normal_r2,0,3220.186,組,会計,10,4.2,11.9,4.2,11.9,NorthAisle,600.027,卓を空けた
w100_normal_r2,0,3230.246,客,ドリンクバー,28,4.55,9.5,7.3502,4.6853,ColumnAisle02_S,60,移した
w100_normal_r2,0,3250.616,スタッフ,料理を運ぶ,2,11.15,13.3,4.3745,7.5952,CrossAisle,60.0494,あきらめた
w100_normal_r2,0,3327.941,組,会計,4,1.9,6.475,1.9,6.475,CrossAisle,600.027,卓を空けた
w100_normal_r2,0,3332.396,スタッフ,料理を運ぶ,11,11.15,13.3,11.7,12.9487,MainAisle,60.0494,あきらめた
w100_normal_r2,0,3381.894,スタッフ,料理を運ぶ,26,11.15,13.3,11.4168,12.6199,MainAisle,60.0494,あきらめた
w100_normal_r2,0,3392.554,スタッフ,料理を運ぶ,11,11.15,13.3,11.7,12.9809,MainAisle,60.0494,あきらめた
w100_normal_r2,0,3424.836,組,会計,6,1.9,10.175,1.9,10.175,WestAisle_N,600.027,卓を空けた
w100_normal_r2,0,3442.052,スタッフ,料理を運ぶ,26,11.15,13.3,11.4005,12.615,MainAisle,60.0494,あきらめた
w100_normal_r2,0,3452.713,スタッフ,料理を運ぶ,11,11.15,13.3,11.7,12.9763,MainAisle,60.0494,移した
w100_normal_r2,0,3502.211,スタッフ,料理を運ぶ,26,11.15,13.3,11.7,12.9706,MainAisle,60.0494,移した
w100_normal_r2,0,3580.888,客,ドリンクバー,35,9.85,10.8,10.25,4.5,ColumnAisle03_S,60,移した
w100_normal_r2,0,3625.682,スタッフ,料理を運ぶ,22,11.15,13.3,7.4832,6.9394,CrossAisle,60.0494,あきらめた
w100_normal_r2,0,3708.312,組,会計,3,1.9,4.625,1.9,4.625,WestAisle_S,600.027,卓を空けた
w100_normal_r2,0,3732.636,組,会計,9,2.75,11.9,2.75,11.9,NorthAisle,600.027,卓を空けた
w100_normal_r2,0,3791.193,組,会計,2,1.9,2.775,1.9,2.775,WestAisle_S,600.027,卓を空けた
w100_normal_r2,0,3844.345,スタッフ,案内,12,7.1,11.9,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,3904.504,スタッフ,案内,12,13.45,0.8,6.4136,12.5068,,60.0494,あきらめた
w100_normal_r2,0,4019.466,スタッフ,料理を運ぶ,34,9.85,9.5,11.7,11.4134,MainAisle,60.0494,あきらめた
w100_normal_r2,0,4032.629,客,ドリンクバー,35,9.85,10.8,10.25,4.5,ColumnAisle03_S,60,移した
w100_normal_r2,0,4079.625,スタッフ,料理を運ぶ,34,11.15,13.3,11.7,12.375,MainAisle,60.0494,あきらめた
w100_normal_r2,0,4080.125,組,会計,11,5.65,11.9,5.65,11.9,NorthAisle,600.027,卓を空けた
w100_normal_r2,0,4276.981,客,ドリンクバー,35,9.85,10.8,10.25,4.5,ColumnAisle03_S,60,移した
w100_normal_r2,0,4294.811,スタッフ,料理を運ぶ,23,11.15,13.3,7.95,12.55,,60.0494,あきらめた
w100_normal_r2,0,4734.188,組,会計,34,9.85,9.5,9.85,9.5,ColumnAisle03_N,600.027,卓を空けた
w100_normal_r2,0,4887.885,客,ドリンクバー,28,4.55,9.5,7.35,3.5071,ColumnAisle02_S,60,移した
w100_normal_r2,0,5029.48,スタッフ,案内,11,5.65,11.9,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,5039.242,スタッフ,料理を運ぶ,7,11.15,13.3,11.1629,12.3204,MainAisle,60.0494,あきらめた
w100_normal_r2,0,5082.124,組,会計,35,9.85,10.8,9.85,10.8,ColumnAisle03_N,600.027,卓を空けた
w100_normal_r2,0,5089.345,スタッフ,案内,11,13.45,0.8,4.7285,10.7814,ColumnAisle01_N,60.0494,あきらめた
w100_normal_r2,0,5145.973,スタッフ,料理を運ぶ,13,8.55,11.9,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,5147.716,組,会計,33,9.85,8.2,9.85,8.2,ColumnAisle03_N,600.027,卓を空けた
w100_normal_r2,0,5205.838,スタッフ,料理を運ぶ,13,11.15,13.3,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,5270.435,スタッフ,料理を運ぶ,13,8.55,11.9,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,5330.3,スタッフ,料理を運ぶ,13,11.15,13.3,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,5386.181,スタッフ,料理を運ぶ,8,11.15,13.3,11.178,12.3669,MainAisle,60.0494,あきらめた
w100_normal_r2,0,5390.166,スタッフ,料理を運ぶ,13,11.15,13.3,9.4956,12.4139,,60.0494,移した
w100_normal_r2,0,5496.698,スタッフ,料理を運ぶ,13,11.15,13.3,8.05,12.3376,NorthAisle,60.0494,あきらめた
w100_normal_r2,0,5588.538,スタッフ,料理を運ぶ,12,7.1,11.9,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,5648.403,スタッフ,料理を運ぶ,12,11.15,13.3,7.9125,12.5417,,60.0494,あきらめた
w100_normal_r2,0,5653.433,客,トイレ,7,1.9,12.025,9.275,12.5,,60,移した
w100_normal_r2,0,5681.672,スタッフ,料理を運ぶ,1,11.15,13.3,11.004,12.0427,MainAisle,60.0494,あきらめた
w100_normal_r2,0,5707.77,組,会計,27,4.55,8.2,4.55,8.2,ColumnAisle01_N,600.027,卓を空けた
w100_normal_r2,0,5708.268,スタッフ,料理を運ぶ,12,11.15,13.3,9.275,12.5,,60.0494,移した
w100_normal_r2,0,5713,組,会計,28,4.55,9.5,4.55,9.5,ColumnAisle01_N,600.027,卓を空けた
w100_normal_r2,0,5768.083,スタッフ,料理を運ぶ,12,11.15,13.3,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,5827.949,スタッフ,料理を運ぶ,12,11.15,13.3,10.25,9.5,ColumnAisle03_N,60.0494,あきらめた
w100_normal_r2,0,5936.174,スタッフ,料理を運ぶ,12,11.15,13.3,9.275,12.5,,60.0494,移した
w100_normal_r2,0,5954.253,スタッフ,案内,34,9.85,9.5,10.25,4.5,ColumnAisle03_S,60.0494,あきらめた
w100_normal_r2,0,6012.973,組,会計,26,9.85,5.8,9.85,5.8,ColumnAisle03_S,600.027,卓を空けた
w100_normal_r2,0,6014.119,スタッフ,案内,34,13.45,0.8,10.25,4.5,ColumnAisle03_S,60.0494,あきらめた
w100_normal_r2,0,6136.29,客,ドリンクバー,13,8.55,11.9,10.25,9.5,ColumnAisle03_N,60,移した
w100_normal_r2,0,6163.981,スタッフ,料理を運ぶ,10,11.15,13.3,7.3172,11.775,NorthAisle,60.0494,あきらめた
w100_normal_r2,0,6271.659,スタッフ,料理を運ぶ,3,11.15,13.3,6.9542,12.1,NorthAisle,60.0494,あきらめた
w100_normal_r2,0,6314.64,スタッフ,料理を運ぶ,6,11.15,13.3,6.591,12.2015,NorthAisle,60.0494,あきらめた
w100_normal_r2,0,6331.524,スタッフ,呼び出し,14,10,11.9,6.55,12.7,,60.0494,あきらめた
w100_normal_r2,0,6333.018,客,トイレ,13,13.45,3.6,7.35,9.7166,ColumnAisle02_N,60,あきらめた
w100_normal_r2,0,6358.418,客,ドリンクバー,13,9,0.15,7.4,9.2735,ColumnAisle02_N,60,あきらめた
w100_normal_r2,0,6419.081,組,会計,24,9.85,3.2,9.85,3.2,ColumnAisle03_S,600.027,卓を空けた
w100_normal_r2,0,6509.277,組,会計,19,4.55,4.5,4.55,4.5,ColumnAisle01_S,600.027,卓を空けた
w100_normal_r2,0,6535.574,組,会計,22,7.2,4.5,7.2,4.5,ColumnAisle02_S,600.027,卓を空けた
w100_normal_r2,0,6535.922,組,会計,18,4.55,3.2,4.55,3.2,ColumnAisle01_S,600.027,卓を空けた
w100_normal_r2,0,6550.914,スタッフ,料理を運ぶ,9,11.15,13.3,11.3508,12.4151,MainAisle,60.0494,あきらめた
w100_normal_r2,0,6576.961,スタッフ,料理を運ぶ,2,11.15,13.3,11.7,12.7752,MainAisle,60.0494,あきらめた
w100_normal_r2,0,6610.779,スタッフ,料理を運ぶ,9,11.15,13.3,11.3505,12.4158,MainAisle,60.0494,あきらめた
w100_normal_r2,0,6636.827,スタッフ,料理を運ぶ,3,11.15,13.3,11.7,12.8879,MainAisle,60.0494,あきらめた
w100_normal_r2,0,6670.644,スタッフ,料理を運ぶ,9,11.15,13.3,11.7,12.4864,MainAisle,60.0494,あきらめた
w100_normal_r2,0,6696.692,スタッフ,料理を運ぶ,3,11.15,13.3,11.7,12.9703,MainAisle,60.0494,あきらめた
w100_normal_r2,0,6698.037,組,会計,14,10,11.9,10,11.9,NorthAisle,600.027,卓を空けた
w100_normal_r2,0,6730.509,スタッフ,料理を運ぶ,9,11.15,13.3,11.3936,12.6009,MainAisle,60.0494,移した
w100_normal_r2,0,6756.557,スタッフ,料理を運ぶ,3,11.15,13.3,11.7,12.8646,MainAisle,60.0494,移した
w100_normal_r2,0,6924.1,組,会計,17,6.4,2.1,6.4,2.1,SouthAisle,600.027,卓を空けた
w100_normal_r2,0,6951.542,客,ドリンクバー,13,8.55,11.9,7.396,5.6059,ColumnAisle02_S,60,移した
w100_normal_r2,0,6990.789,スタッフ,料理を運ぶ,11,11.15,13.3,7.9115,12.5445,,60.0494,あきらめた
w100_normal_r2,0,7057.029,組,会計,16,4.575,2.1,4.575,2.1,SouthAisle,600.027,卓を空けた
w100_normal_r2,0,7057.128,組,会計,25,9.85,4.5,9.85,4.5,ColumnAisle03_S,600.027,卓を空けた
w100_normal_r2,0,7156.14,客,ドリンクバー,12,7.1,11.9,6.3923,12.4897,,60,移した
w100_normal_r2,0,7223.924,組,会計,7,1.9,12.025,1.9,12.025,NorthAisle,600.027,卓を空けた
w100_normal_r2,0,7343.804,スタッフ,料理を運ぶ,13,8.55,11.9,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,7382.602,組,会計,20,4.55,5.8,4.55,5.8,ColumnAisle01_S,600.027,卓を空けた
w100_normal_r2,0,7403.669,スタッフ,料理を運ぶ,13,11.15,13.3,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,7556.62,スタッフ,料理を運ぶ,35,9.85,10.8,11.7,13.6942,MainAisle,60.0494,あきらめた
w100_normal_r2,0,7636.158,スタッフ,呼び出し,13,8.55,11.9,9.275,12.5,,60.0494,あきらめた
w100_normal_r2,0,7645.571,客,ドリンクバー,12,8.1,0.15,10.25,9.7682,ColumnAisle03_N,60,あきらめた
w100_normal_r2,0,7685.664,スタッフ,案内,14,10,11.9,10.3693,9.232,MainAisle,60.0494,あきらめた
w100_normal_r2,0,7710.865,組,会計,23,7.2,5.8,7.2,5.8,ColumnAisle02_S,600.027,卓を空けた
w100_normal_r2,0,7775.063,客,ドリンクバー,12,7.1,11.9,7.7492,7.3622,CrossAisle,60,移した
w100_normal_r2,0,7853.854,スタッフ,料理を運ぶ,15,11.15,13.3,7.9962,12.55,,60.0494,あきらめた
w100_normal_r2,0,7880.5,スタッフ,料理を運ぶ,33,11.15,13.3,11.4945,12.2867,MainAisle,60.0494,あきらめた
w100_normal_r2,0,7935.334,客,ドリンクバー,10,4.2,11.9,4.55,9.0467,ColumnAisle01_N,60,移した
w100_normal_r2,0,8012.382,スタッフ,料理を運ぶ,33,9.85,8.2,11.7,11.1776,MainAisle,60.0494,あきらめた
w100_normal_r2,0,8054.965,組,会計,13,8.55,11.9,8.55,11.9,NorthAisle,600.027,卓を空けた
contacts.csv
condition_id,seed,t_s,x,z,aisle,place,who,careless,robot_id,gap_cm
w100_normal_r2,0,3.42,11.1191,11.9511,MainAisle,,スタッフ:Staff01,0,2,9.9824
w100_normal_r2,0,32.2198,11.5226,13.7028,MainAisle,,スタッフ:Staff01,0,1,9.8684
w100_normal_r2,0,48.3196,11.7,12.5947,MainAisle,,スタッフ:Staff02,0,1,9.754
w100_normal_r2,0,51.2195,11.7,12.5265,MainAisle,,スタッフ:Staff01,0,1,9.8609
w100_normal_r2,0,59.2694,10.2604,4.7697,ColumnAisle03_S,,客:Guest(Clone),0,2,9.5904
w100_normal_r2,0,84.6206,10.25,2.78,ColumnAisle03_S,,客:Guest(Clone),0,2,8.5034
w100_normal_r2,0,96.8213,10.25,2.78,ColumnAisle03_S,,客:Guest(Clone),0,2,8.5034
w100_normal_r2,0,104.3218,10.25,2.78,ColumnAisle03_S,,客:Guest(Clone),0,2,8.5033
w100_normal_r2,0,112.7723,10.25,2.7804,ColumnAisle03_S,,客:Guest(Clone),0,2,8.5254
w100_normal_r2,0,115.1724,10.1948,2.6856,ColumnAisle03_S,,客:Guest(Clone),0,2,9.3181
w100_normal_r2,0,192.8272,10.3529,11.9528,MainAisle,,スタッフ:Staff02,0,1,9.539
w100_normal_r2,0,192.9772,10.3889,2.8469,MainAisle,,客:Guest(Clone),0,2,9.4722
w100_normal_r2,0,199.8276,9.2171,12.2561,NorthAisle,,スタッフ:Staff01,0,1,9.8249
w100_normal_r2,0,209.9282,10.5216,3.8492,MainAisle,,客:Guest(Clone),0,2,9.8529
w100_normal_r2,0,217.6787,10.821,4.6742,MainAisle,,客:Guest(Clone),0,2,9.4448
w100_normal_r2,0,241.3301,11.7,11.3482,MainAisle,,スタッフ:Staff02,0,2,9.652
w100_normal_r2,0,322.4148,6.6433,6.9578,CrossAisle,,スタッフ:Staff01,0,2,8.0505
w100_normal_r2,0,789.2008,10.3538,9.169,MainAisle,,スタッフ:Staff01,0,1,9.8922
w100_normal_r2,0,808.896,10.6432,4.3427,MainAisle,,客:Guest(Clone),0,1,9.2199
w100_normal_r2,0,908.1718,7.3506,6.3622,ColumnAisle02_S,,客:Guest(Clone),0,2,9.2528
w100_normal_r2,0,935.3651,7.3786,6.5675,CrossAisle,,客:Guest(Clone),0,2,9.6703
w100_normal_r2,0,1037.857,9.1728,12.1616,NorthAisle,,スタッフ:Staff01,0,1,9.3087
w100_normal_r2,0,1077.396,7.612,7.129,CrossAisle,,客:Guest(Clone),0,1,9.3254
w100_normal_r2,0,1096.414,10.3016,8.4382,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.6076
w100_normal_r2,0,1097.415,10.2534,8.5307,ColumnAisle03_N,,客:Guest(Clone),1,2,8.1501
w100_normal_r2,0,1105.223,10.2634,9.064,ColumnAisle03_N,,スタッフ:Staff02,0,2,7.3681
w100_normal_r2,0,1109.527,7.7563,7.2106,CrossAisle,,スタッフ:Staff02,0,1,7.5908
w100_normal_r2,0,1139.006,7.1766,7.2109,CrossAisle,,スタッフ:Staff02,0,1,8.4402
w100_normal_r2,0,1143.31,10.3109,8.4244,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.8381
w100_normal_r2,0,1198.163,7.2396,11.8197,NorthAisle,,客:Guest(Clone),0,1,9.3711
w100_normal_r2,0,1215.43,7.1681,11.7819,NorthAisle,,客:Guest(Clone),0,1,9.7724
w100_normal_r2,0,1270.284,11.0523,9.2139,MainAisle,,スタッフ:Staff02,0,2,8.4176
w100_normal_r2,0,1288.001,10.2988,8.4626,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.498
w100_normal_r2,0,1429.439,7.2903,11.8437,NorthAisle,,客:Guest(Clone),0,1,9.5538
w100_normal_r2,0,1522.48,10.3031,9.1473,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.24
w100_normal_r2,0,1545.652,10.25,9.075,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.2828
w100_normal_r2,0,1549.906,10.3106,8.4439,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.222
w100_normal_r2,0,1553.56,10.2881,8.4717,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.1329
w100_normal_r2,0,1576.733,10.2999,9.1427,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.0073
w100_normal_r2,0,1587.443,7.6865,11.8412,NorthAisle,,スタッフ:Staff02,0,1,9.7965
w100_normal_r2,0,1633.288,7.3837,10.7985,ColumnAisle02_N,,スタッフ:Staff02,0,1,9.3345
w100_normal_r2,0,1704.708,10.2936,11.9362,NorthAisle,,スタッフ:Staff02,0,1,9.0007
w100_normal_r2,0,1799.15,10.25,9.075,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.7615
w100_normal_r2,0,1803.404,10.3116,8.4401,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.996
w100_normal_r2,0,1826.076,10.2843,8.4575,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.9478
w100_normal_r2,0,1840.74,10.3021,9.1314,ColumnAisle03_N,,スタッフ:Staff01,0,2,7.6848
w100_normal_r2,0,1887.186,7.35,10.9536,ColumnAisle02_N,,スタッフ:Staff02,0,1,9.5487
w100_normal_r2,0,1941.138,7.6781,11.9779,NorthAisle,,スタッフ:Staff01,0,1,8.0469
w100_normal_r2,0,2020.265,10.285,9.1592,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.6756
w100_normal_r2,0,2045.49,10.2812,8.4526,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.0336
w100_normal_r2,0,2056,7.35,10.81,ColumnAisle02_N,,スタッフ:Staff01,0,1,8.9274
w100_normal_r2,0,2067.762,10.293,9.1821,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.4708
w100_normal_r2,0,2110.704,7.35,10.8058,ColumnAisle02_N,,スタッフ:Staff01,0,1,9.3353
w100_normal_r2,0,2292.331,10.2993,9.1438,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.0673
w100_normal_r2,0,2303.742,7.2396,11.8197,NorthAisle,,客:Guest(Clone),0,1,9.3711
w100_normal_r2,0,2323.862,10.2995,9.1326,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.3363
w100_normal_r2,0,2373.21,7.1862,11.7638,NorthAisle,,客:Guest(Clone),0,1,9.7139
w100_normal_r2,0,2374.461,7.4734,12.1185,NorthAisle,,スタッフ:Staff01,0,1,9.0343
w100_normal_r2,0,2528.761,7.4,10.7904,ColumnAisle02_N,,客:Guest(Clone),0,1,9.8644
w100_normal_r2,0,2558.891,7.5867,11.74,NorthAisle,,客:Guest(Clone),0,1,8.3156
w100_normal_r2,0,2559.842,7.5783,11.7202,NorthAisle,,スタッフ:Staff02,1,1,9.2717
w100_normal_r2,0,2573.105,10.3038,9.1661,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.5984
w100_normal_r2,0,2637.718,10.2501,9.0668,ColumnAisle03_N,,客:Guest(Clone),0,2,8.1818
w100_normal_r2,0,2659.789,10.25,9.0656,ColumnAisle03_N,,客:Guest(Clone),0,2,8.1301
w100_normal_r2,0,2660.99,10.25,10.42,ColumnAisle03_N,,客:Guest(Clone),0,1,9.631
w100_normal_r2,0,2674.053,10.25,10.1162,ColumnAisle03_N,,客:Guest(Clone),0,1,8.8962
w100_normal_r2,0,2718.747,10.25,9.1,ColumnAisle03_N,,客:Guest(Clone),0,2,9.9082
w100_normal_r2,0,2781.258,7.3838,11.0164,ColumnAisle02_N,,客:Guest(Clone),0,1,9.3205
w100_normal_r2,0,2785.011,10.25,8.4618,ColumnAisle03_N,,客:Guest(Clone),0,2,9.8296
w100_normal_r2,0,2787.964,7.4,10.975,ColumnAisle02_N,,スタッフ:Staff01,0,1,8.5738
w100_normal_r2,0,2876.851,11.7,12.472,MainAisle,,スタッフ:Staff01,0,2,9.9218
w100_normal_r2,0,2887.361,10.3044,8.4372,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.4548
w100_normal_r2,0,2924.648,10.25,9.0661,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.632
w100_normal_r2,0,2938.361,10.302,8.4355,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.7523
w100_normal_r2,0,2966.489,10.3117,8.4227,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.8845
w100_normal_r2,0,2990.812,10.2844,9.1099,ColumnAisle03_N,,スタッフ:Staff02,0,2,7.9667
w100_normal_r2,0,2991.863,10.3026,9.1345,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.0258
w100_normal_r2,0,3076.045,9.8041,12.1,NorthAisle,,スタッフ:Staff01,0,1,8.7963
w100_normal_r2,0,3118.837,10.2544,9.1398,ColumnAisle03_N,,客:Guest(Clone),0,2,9.6915
w100_normal_r2,0,3189.106,5.7163,6.8163,CrossAisle,,スタッフ:Staff02,0,2,8.7338
w100_normal_r2,0,3199.566,4.3482,7.5646,CrossAisle,,スタッフ:Staff01,0,2,9.2992
w100_normal_r2,0,3253.819,6.7503,7.0883,CrossAisle,,スタッフ:Staff01,0,1,8.6727
w100_normal_r2,0,3266.331,11.4,13.3,MainAisle,,スタッフ:Staff01,0,2,-20
w100_normal_r2,0,3314.278,11.7,12.5972,MainAisle,,スタッフ:Staff02,0,2,9.8783
w100_normal_r2,0,3501.01,10.25,8.4751,ColumnAisle03_N,,客:Guest(Clone),0,2,9.0555
w100_normal_r2,0,3502.211,11.15,13.3,MainAisle,,スタッフ:Staff01,0,1,-45
w100_normal_r2,0,3505.164,11.7,12.662,MainAisle,,スタッフ:Staff02,0,1,9.9707
w100_normal_r2,0,3507.516,10.2881,9.1811,ColumnAisle03_N,,客:Guest(Clone),0,2,9.6914
w100_normal_r2,0,3546.404,10.5473,9.3269,MainAisle,,スタッフ:Staff02,0,2,9.2761
w100_normal_r2,0,3650.055,10.3022,8.4543,ColumnAisle03_N,,スタッフ:Staff01,0,1,8.5174
w100_normal_r2,0,3664.62,10.2981,9.1304,ColumnAisle03_N,,スタッフ:Staff01,0,1,8.3043
w100_normal_r2,0,3689.094,7.4047,7.1206,CrossAisle,,スタッフ:Staff01,0,2,8.7988
w100_normal_r2,0,3796.548,10.694,8.2565,MainAisle,,スタッフ:Staff02,0,2,9.2852
w100_normal_r2,0,3822.023,10.2557,8.5075,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.2468
w100_normal_r2,0,3848.899,11.0438,9.2361,MainAisle,,スタッフ:Staff02,0,2,9.8041
w100_normal_r2,0,3907.106,7.3109,11.8538,NorthAisle,,スタッフ:Staff02,0,1,9.8253
w100_normal_r2,0,3941.49,10.306,8.4517,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.6445
w100_normal_r2,0,3957.355,10.3103,9.1755,ColumnAisle03_N,,客:Guest(Clone),0,2,9.9542
w100_normal_r2,0,3986.484,10.942,10.5807,MainAisle,,スタッフ:Staff02,0,1,9.0217
w100_normal_r2,0,3988.285,10.3939,9.2427,MainAisle,,スタッフ:Staff02,0,2,9.4422
w100_normal_r2,0,4051.797,10.7467,9.6099,MainAisle,,スタッフ:Staff02,0,1,9.6211
w100_normal_r2,0,4136.582,10.4545,9.2963,MainAisle,,スタッフ:Staff02,0,2,-2.1487
w100_normal_r2,0,4138.624,10.25,9.2593,ColumnAisle03_N,,客:Guest(Clone),0,2,9.3762
w100_normal_r2,0,4161.534,10.2966,9.3189,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.6884
w100_normal_r2,0,4196.795,10.274,8.5873,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.1757
w100_normal_r2,0,4210.392,10.3248,10.3206,ColumnAisle03_N,,スタッフ:Staff02,0,1,9.0776
w100_normal_r2,0,4212.982,10.293,8.5531,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.8382
w100_normal_r2,0,4298.347,9.8218,12.1,NorthAisle,,スタッフ:Staff02,0,1,9.5852
w100_normal_r2,0,4305.469,10.9603,10.8956,MainAisle,,スタッフ:Staff02,0,2,8.8868
w100_normal_r2,0,4338.888,10.2844,8.4608,ColumnAisle03_N,,スタッフ:Staff02,0,2,7.6589
w100_normal_r2,0,4362.147,10.2949,9.1849,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.9565
w100_normal_r2,0,4393.574,10.2832,8.4301,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.7746
w100_normal_r2,0,4444.723,10.2818,9.1489,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.2939
w100_normal_r2,0,4466.289,10.2799,8.4371,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.688
w100_normal_r2,0,4487.754,10.25,9.1218,ColumnAisle03_N,,客:Guest(Clone),0,2,8.9278
w100_normal_r2,0,4492.685,10.2814,9.1556,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.5709
w100_normal_r2,0,4524.012,10.2785,8.4391,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.7963
w100_normal_r2,0,4539.302,10.2891,9.1619,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.4157
w100_normal_r2,0,4560.768,10.2844,9.1671,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.1092
w100_normal_r2,0,4594.087,10.2867,8.4557,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.2144
w100_normal_r2,0,4756.799,10.2915,9.1851,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.2188
w100_normal_r2,0,4798.137,10.2845,9.1824,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.5492
w100_normal_r2,0,4798.685,10.3347,8.4086,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.4679
w100_normal_r2,0,4830.162,10.2835,8.4502,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.2241
w100_normal_r2,0,4844.555,10.2894,9.1774,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.6526
w100_normal_r2,0,4982.365,10.2945,8.4773,ColumnAisle03_N,,スタッフ:Staff01,0,2,7.8046
w100_normal_r2,0,4999.846,9.275,12.5,,,スタッフ:Staff02,0,1,9.5389
w100_normal_r2,0,5074.802,7.6698,11.8453,NorthAisle,,スタッフ:Staff01,0,1,9.676
w100_normal_r2,0,5081.327,10.2815,8.4695,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.5414
w100_normal_r2,0,5192.889,10.3075,8.4447,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.2647
w100_normal_r2,0,5255.743,9.9562,12.0438,NorthAisle,,スタッフ:Staff01,0,1,9.2444
w100_normal_r2,0,5346.985,9.275,12.5,,,スタッフ:Staff02,0,1,9.678
w100_normal_r2,0,5391.61,10.3001,9.1738,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.8079
w100_normal_r2,0,5445.548,8.0593,12.2907,NorthAisle,,スタッフ:Staff01,0,1,9.0609
w100_normal_r2,0,5536.193,10.2979,9.1329,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.2904
w100_normal_r2,0,5573.596,10.3093,8.4428,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.8543
w100_normal_r2,0,5597.154,10.3057,9.1457,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.5026
w100_normal_r2,0,5625.194,11.2553,8.8289,MainAisle,,客:Guest(Clone),0,2,9.3132
w100_normal_r2,0,5806.333,10.4773,9.3037,MainAisle,,スタッフ:Staff01,0,2,9.9904
w100_normal_r2,0,5809.471,10.25,9.0763,ColumnAisle03_N,,客:Guest(Clone),0,2,8.506
w100_normal_r2,0,5814.95,10.302,9.1601,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.1512
w100_normal_r2,0,5819.333,10.3308,8.4109,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.9332
w100_normal_r2,0,5857.632,10.2579,8.5273,ColumnAisle03_N,,客:Guest(Clone),0,2,8.3021
w100_normal_r2,0,5950.518,10.3573,9.2174,MainAisle,,スタッフ:Staff01,0,2,9.3954
w100_normal_r2,0,5990.81,9.8081,12.0996,NorthAisle,,スタッフ:Staff01,1,1,9.0646
w100_normal_r2,0,5997.285,10.3859,12.1573,MainAisle,,スタッフ:Staff01,0,1,9.1949
w100_normal_r2,0,6026.57,10.2854,8.4451,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.1574
w100_normal_r2,0,6074.432,10.2843,9.1576,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.0836
w100_normal_r2,0,6081.903,10.2871,9.1594,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.0038
w100_normal_r2,0,6133.351,10.2833,8.473,ColumnAisle03_N,,客:Guest(Clone),0,2,8.9544
w100_normal_r2,0,6148.591,7.3079,11.8488,NorthAisle,,スタッフ:Staff02,0,1,9.3998
w100_normal_r2,0,6190.079,10.3024,9.137,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.0153
w100_normal_r2,0,6227.93,10.3024,8.4586,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.48
w100_normal_r2,0,6266.28,6.6499,12.2441,NorthAisle,,スタッフ:Staff02,0,1,9.1263
w100_normal_r2,0,6273.103,6.6371,11.9094,NorthAisle,,スタッフ:Staff01,0,1,9.6993
w100_normal_r2,0,6294.419,7.6861,11.8356,NorthAisle,,客:Guest(Clone),0,1,9.1302
w100_normal_r2,0,6303.683,7.3633,8.1867,ColumnAisle02_N,,客:Guest(Clone),0,2,9.3753
w100_normal_r2,0,6308.514,6.6657,11.9714,NorthAisle,,スタッフ:Staff02,0,1,9.4197
w100_normal_r2,0,6319.82,7.6893,11.835,NorthAisle,,客:Guest(Clone),0,1,9.4368
w100_normal_r2,0,6326.693,6.7337,12.0768,NorthAisle,,スタッフ:Staff01,0,1,9.3892
w100_normal_r2,0,6335.16,6.7565,12.1,NorthAisle,,スタッフ:Staff02,0,1,9.1854
w100_normal_r2,0,6372.513,8.6248,6.8248,CrossAisle,,客:Guest(Clone),0,1,9.9439
w100_normal_r2,0,6421.571,10.2993,9.1317,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.2935
w100_normal_r2,0,6423.912,10.2958,9.1495,ColumnAisle03_N,,客:Guest(Clone),0,2,9.6959
w100_normal_r2,0,6443.435,6.0623,6.9,CrossAisle,,スタッフ:Staff01,0,2,8.5157
w100_normal_r2,0,6448.316,6.0544,6.9,CrossAisle,,スタッフ:Staff02,0,2,8.0974
w100_normal_r2,0,6467.391,5.7885,6.9001,CrossAisle,,スタッフ:Staff02,0,1,9.8802
w100_normal_r2,0,6498.668,6.3773,6.9,CrossAisle,,スタッフ:Staff02,0,1,8.923
w100_normal_r2,0,6521.23,5.8083,6.9,CrossAisle,,スタッフ:Staff01,0,1,8.8174
w100_normal_r2,0,6536.62,11.7,12.7925,MainAisle,,スタッフ:Staff02,0,2,9.4453
w100_normal_r2,0,6730.509,11.15,13.3,MainAisle,,スタッフ:Staff01,0,1,-45
w100_normal_r2,0,6766.767,10.3068,9.1644,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.8766
w100_normal_r2,0,6790.076,7.2495,7.154,CrossAisle,,スタッフ:Staff01,0,1,9.847
w100_normal_r2,0,6794.309,10.3084,8.4461,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.5873
w100_normal_r2,0,6815.127,10.3034,9.1316,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.0003
w100_normal_r2,0,6856.914,10.3048,9.1612,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.6161
w100_normal_r2,0,6894.167,10.3037,8.441,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.4057
w100_normal_r2,0,6894.167,7.3348,11.8588,NorthAisle,,客:Guest(Clone),0,1,9.6069
w100_normal_r2,0,6961.155,7.3146,11.8561,NorthAisle,,スタッフ:Staff01,0,1,9.9293
w100_normal_r2,0,6996.417,11.7,12.6262,MainAisle,,スタッフ:Staff01,0,1,9.6539
w100_normal_r2,0,7004.385,10.3078,9.165,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.261
w100_normal_r2,0,7077.1,7.2396,11.8197,NorthAisle,,客:Guest(Clone),0,1,9.3711
w100_normal_r2,0,7096.175,10.25,9.0628,ColumnAisle03_N,,スタッフ:Staff01,0,2,7.868
w100_normal_r2,0,7097.271,10.3043,9.1524,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.501
w100_normal_r2,0,7118.289,10.2992,8.4452,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.8602
w100_normal_r2,0,7129.843,10.2905,8.4734,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.7307
w100_normal_r2,0,7152.704,10.3013,9.1413,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.0249
w100_normal_r2,0,7237.521,10.25,9.0661,ColumnAisle03_N,,スタッフ:Staff02,0,2,7.8758
w100_normal_r2,0,7416.519,10.3055,9.1455,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.5083
w100_normal_r2,0,7420.952,10.3252,8.4264,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.797
w100_normal_r2,0,7586.403,7.2396,11.8197,NorthAisle,,客:Guest(Clone),0,1,9.3711
w100_normal_r2,0,7637.602,10.2904,8.5327,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.7514
w100_normal_r2,0,7687.008,10.25,9.2323,ColumnAisle03_N,,客:Guest(Clone),0,2,8.9427
w100_normal_r2,0,7695.923,7.2396,11.8197,NorthAisle,,客:Guest(Clone),0,1,9.3711
w100_normal_r2,0,7713.006,10.2901,8.5352,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.748
w100_normal_r2,0,7732.928,10.2834,12.0304,NorthAisle,,スタッフ:Staff02,0,1,9.7308
w100_normal_r2,0,7770.332,10.2879,9.2772,ColumnAisle03_N,,スタッフ:Staff02,0,2,8.6554
w100_normal_r2,0,7858.635,9.7886,12.0924,NorthAisle,,スタッフ:Staff02,0,1,9.5946
w100_normal_r2,0,7871.335,10.9274,10.7612,MainAisle,,スタッフ:Staff02,0,2,9.5205
w100_normal_r2,0,7897.483,10.2724,8.6953,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.9061
w100_normal_r2,0,7931.948,10.2568,9.2008,ColumnAisle03_N,,客:Guest(Clone),0,2,9.8236
w100_normal_r2,0,7944.2,10.2704,9.6477,ColumnAisle03_N,,スタッフ:Staff02,0,2,9.0425
w100_normal_r2,0,8061.34,10.2724,8.6953,ColumnAisle03_N,,スタッフ:Staff01,0,2,8.9061
w100_normal_r2,0,8071.052,10.3108,9.2663,ColumnAisle03_N,,スタッフ:Staff01,0,2,9.0499
w100_normal_r2,0,8076.431,10.25,9.16,ColumnAisle03_N,,客:Guest(Clone),0,2,8.5205
people_jams.csv
condition_id,seed,t_s,x,z,aisle,people,duration_s,resolved,robot_blocked,robot_id
w100_normal_r2,0,3.82,11.7,10.4852,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,5.07,11.7,10.4829,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6.32,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7.57,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,8.82,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,10.07,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,11.32,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,12.57,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,13.82,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,15.07,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,16.32,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,17.57,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,18.82,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,20.07,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,21.32,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,22.5699,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,23.8199,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,25.0699,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,26.3199,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,27.5699,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,28.8199,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,30.0698,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,31.3198,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,32.5698,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,33.8198,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,35.0698,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,36.3197,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,37.5697,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,38.8197,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,40.0697,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,41.3197,11.7,10.4822,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,42.5696,11.7,10.4832,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,43.8196,11.7,10.4832,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,72.8199,4.8,8.602,ColumnAisle01_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,75.27,4.8,8.728,ColumnAisle01_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,81.4704,4.7003,8.8738,ColumnAisle01_N,4,1.1,体を横に向けて抜けた,0,0
w100_normal_r2,0,89.9709,4.8,8.4933,ColumnAisle01_N,3,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,91.171,4.8,8.0013,ColumnAisle01_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,92.371,4.8,8.0385,ColumnAisle01_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,94.0711,4.8,8.6305,ColumnAisle01_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,95.3212,4.8,8.1474,ColumnAisle01_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,95.3712,4.55,7.8573,ColumnAisle01_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,147.3744,4.55,6.4606,CrossAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,148.6245,4.55,6.4341,CrossAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,148.7245,4.8,6.1303,ColumnAisle01_S,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,150.0246,4.55,6.3826,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,151.2746,4.55,6.3561,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,151.3746,4.8,6.0523,ColumnAisle01_S,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,152.7247,4.55,6.3197,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,152.7747,4.8,6.0168,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,154.6748,4.55,6.204,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,155.9749,4.55,6.1791,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,156.0249,4.8,5.876,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,157.375,4.55,6.1286,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,158.6751,4.55,6.1026,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,158.7251,4.8,5.7996,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,160.2252,4.55,6.0359,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,161.5253,4.55,6.0111,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,161.5753,4.8,5.7081,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,162.7753,4.55,5.9679,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,163.9254,4.55,5.94,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,201.2277,9.0619,12.2775,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,202.7278,9.0647,12.2752,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,204.2279,9.0647,12.2752,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,205.728,9.0647,12.2752,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,207.228,9.0647,12.2752,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,208.7281,9.0647,12.2752,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,210.2282,9.0647,12.2752,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,211.7283,9.0647,12.2752,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,213.2284,9.0647,12.2752,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,215.5285,2.15,4.3301,WestAisle_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,216.8286,2.15,4.387,WestAisle_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,530.8639,7.4,2.8029,ColumnAisle02_S,2,1.1,体を横に向けて抜けた,0,0
w100_normal_r2,0,556.2077,7.4,5.4026,ColumnAisle02_S,2,1.1,体を横に向けて抜けた,0,0
w100_normal_r2,0,711.6697,7.9247,1.6146,SouthAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,713.0194,7.9271,1.6232,SouthAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,713.0194,7.5911,1.8393,SouthAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,720.9175,4.55,4.8037,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,722.0672,4.55,4.7879,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,723.2169,4.55,4.779,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,724.3666,4.55,4.7703,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,725.5164,4.55,4.7616,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,726.816,4.55,4.7554,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,727.9658,4.55,4.741,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,729.2654,4.55,4.7158,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,730.4152,4.55,4.6852,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,731.5649,4.55,4.656,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,732.7146,4.55,4.6269,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,733.8643,4.55,4.5974,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,735.014,4.55,4.5651,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,736.1638,4.55,4.5315,ColumnAisle01_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,846.7867,7.3598,3.13,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,880.7285,7.4,6.2795,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,882.778,7.339,6.6894,CrossAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,888.3766,7.35,3.0322,ColumnAisle02_S,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,888.3266,7.4,3.4415,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,889.5263,7.35,3.0381,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,889.5263,7.4,3.4306,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,890.826,7.35,3.0338,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,890.826,7.4,3.4261,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,892.0257,7.35,3.0338,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,892.0257,7.4,3.4263,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,936.0649,7.35,6.4761,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,938.1644,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,939.4141,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,940.6638,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,941.9135,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,943.1632,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,944.4129,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,945.6626,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,946.9123,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,948.162,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,949.4117,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,951.4112,7.35,6.4991,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,952.6609,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,953.9106,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,954.5604,7.4,3.5184,ColumnAisle02_S,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,954.5104,7.35,3.9058,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,955.1603,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,956.31,7.4,3.6032,ColumnAisle02_S,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,956.26,7.35,3.9906,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,956.41,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,957.6597,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,958.0596,7.4,3.688,ColumnAisle02_S,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,958.0096,7.35,4.0754,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,958.9094,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,959.8091,7.4,3.7728,ColumnAisle02_S,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,959.7592,7.35,4.1602,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,960.1591,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,961.4088,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,961.5587,7.4,3.8577,ColumnAisle02_S,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,961.5087,7.35,4.2451,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,962.6584,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,963.3083,7.4,3.9425,ColumnAisle02_S,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,963.2583,7.35,4.3299,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,963.9081,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,965.1578,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,966.4075,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,967.6572,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,968.9069,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,970.1566,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,971.4563,7.35,6.4918,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,972.706,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,973.9557,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,975.2054,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,976.4551,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,977.7048,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,978.9545,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,980.2042,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,981.4539,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,982.7036,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,983.9532,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,985.2029,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,986.4526,7.35,6.4909,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,995.9503,4.8453,7.4132,CrossAisle,2,3.05,下がって抜けた,1,2
w100_normal_r2,0,1037.457,6.1267,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1038.858,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1040.259,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1041.661,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1043.062,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1044.463,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1045.865,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1047.266,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1048.667,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1050.069,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1051.47,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1052.872,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1054.273,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1055.674,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1057.076,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1058.477,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1059.878,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1061.28,6.1265,7.35,CrossAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1218.783,7.3104,11.7737,NorthAisle,3,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1220.035,7.3029,11.7721,NorthAisle,3,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1221.286,7.3028,11.7721,NorthAisle,3,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1222.537,7.3028,11.7721,NorthAisle,3,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1223.788,7.3028,11.7721,NorthAisle,3,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1225.04,7.3028,11.7721,NorthAisle,3,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1226.291,7.3028,11.7721,NorthAisle,3,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1227.542,7.3028,11.7721,NorthAisle,3,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1228.793,7.3028,11.7721,NorthAisle,3,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1230.044,7.3028,11.7721,NorthAisle,3,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1231.296,7.3028,11.7721,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1389.9,13.5117,1.0383,,6,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,1391.302,13.5367,1.0134,,6,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,1392.703,13.566,0.984,,6,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,1415.726,8.075,12.1821,NorthAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,1417.077,8.0739,12.1836,NorthAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,1418.578,8.075,12.1832,NorthAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,1419.98,8.075,12.1827,NorthAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,1421.381,8.075,12.1827,NorthAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,1422.782,8.075,12.1827,NorthAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,1424.184,8.075,12.1827,NorthAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,1588.995,7.3205,11.7755,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1633.838,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1635.04,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1636.241,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1637.442,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1638.643,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1639.844,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1641.045,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1642.247,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1643.448,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1644.649,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1645.85,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1647.051,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1648.252,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1649.454,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1650.655,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1651.856,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1653.057,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1654.258,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1655.459,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1656.661,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1657.862,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1659.063,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1660.264,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1661.465,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1662.667,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1663.868,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1665.069,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1666.27,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1667.471,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1668.672,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1669.874,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1671.075,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1672.276,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1673.477,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1674.678,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1675.879,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1677.081,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1678.282,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1679.483,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1680.684,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1681.885,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1683.086,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1684.288,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1685.489,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1686.69,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1687.891,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1689.092,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1690.293,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1691.495,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1692.696,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1693.897,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1695.098,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1696.299,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1697.5,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1698.702,7.4,10.8831,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1828.929,10.6676,9.5373,MainAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1830.53,10.6652,9.5371,MainAisle,2,1.15,体を横に向けて抜けた,1,2
w100_normal_r2,0,1832.132,10.6639,9.537,MainAisle,2,1.15,体を横に向けて抜けた,1,2
w100_normal_r2,0,1833.683,10.6666,9.5372,MainAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1835.285,10.6639,9.537,MainAisle,2,1.15,体を横に向けて抜けた,1,2
w100_normal_r2,0,1836.836,10.6666,9.5372,MainAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,1887.836,7.35,11.067,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1889.087,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1890.339,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1891.59,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1892.841,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1894.092,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1895.344,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1896.595,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1897.846,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1899.097,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1900.348,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1901.6,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1902.851,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1904.102,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1905.353,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1906.604,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1907.856,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1909.107,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1910.358,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1911.609,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1912.861,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1914.112,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1915.363,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1916.614,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1917.865,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1919.117,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1920.368,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1921.619,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1922.87,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1924.122,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1925.373,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1926.624,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1927.875,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1929.126,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1930.378,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1931.629,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1932.88,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1934.131,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1935.383,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1936.634,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1937.885,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1939.136,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1940.387,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1941.639,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1942.89,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1944.141,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1945.392,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1946.644,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1947.895,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1949.146,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1950.397,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1951.648,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1952.9,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1954.151,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,1955.402,7.35,11.0693,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2056.551,7.35,10.887,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2057.752,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2058.953,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2060.154,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2061.355,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2062.557,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2063.758,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2064.959,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2066.16,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2067.361,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2068.563,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2069.764,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2070.965,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2072.166,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2073.567,7.3609,10.8848,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2074.769,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2075.97,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2077.171,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2078.372,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2079.573,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2080.774,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2081.976,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2083.177,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2084.378,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2085.579,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2086.78,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2087.981,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2089.183,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2090.384,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2091.585,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2092.786,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2093.987,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2095.188,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2096.39,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2097.591,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2098.792,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2099.993,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2101.194,7.35,10.886,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2111.304,7.35,10.8875,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2112.505,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2113.707,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2114.908,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2116.109,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2117.31,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2118.511,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2119.712,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2120.914,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2122.115,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2123.316,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2124.517,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2125.718,7.35,10.8885,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2188.93,4.55,8.5068,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2188.93,4.8,8.8058,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2190.281,4.55,8.5066,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2190.281,4.8,8.806,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2191.633,4.55,8.5066,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2191.633,4.8,8.806,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2192.984,4.55,8.5066,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2192.984,4.8,8.806,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2194.335,4.55,8.5066,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2194.335,4.8,8.806,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2195.687,4.55,8.5066,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2195.687,4.8,8.806,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2197.038,4.55,8.5066,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2197.038,4.8,8.806,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2198.389,4.55,8.5067,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2198.389,4.8,8.8066,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2199.74,4.55,8.5071,ColumnAisle01_N,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,2199.74,4.8,8.807,ColumnAisle01_N,2,1.1,体を横に向けて抜けた,0,0
w100_normal_r2,0,2348.486,6.5988,12.3094,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2349.787,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2351.088,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2352.389,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2353.691,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2354.992,6.5989,12.3089,NorthAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,2482.466,4.55,9.5588,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2497.281,7.35,8.9042,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2498.532,7.4,9.3505,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2499.783,7.4,9.4028,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2501.034,7.4,9.4568,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2502.286,7.4,9.511,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2503.537,7.4,9.5653,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2504.788,7.4,9.6197,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2506.039,7.4,9.6742,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2507.291,7.4,9.729,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2508.542,7.4,9.7839,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2526.209,7.4,10.681,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2527.66,7.4,10.7434,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2529.112,7.4,10.8067,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2547.63,7.4,10.8754,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2549.131,7.4,10.94,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2550.383,7.4,10.9908,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2675.004,10.25,9.9168,ColumnAisle03_N,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,2693.122,10.25,4.5926,ColumnAisle03_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2746.824,7.35,8.9004,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2748.125,7.35,8.9576,ColumnAisle02_N,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,2748.075,7.4,9.3455,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2749.377,7.4,9.3999,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2750.628,7.4,9.4531,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2751.879,7.4,9.5063,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2753.13,7.4,9.56,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2754.382,7.4,9.6144,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2755.633,7.4,9.669,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2756.884,7.4,9.7237,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2758.135,7.4,9.7786,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2759.386,7.4,9.8337,ColumnAisle02_N,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,2775.953,7.4,10.6803,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2777.404,7.4,10.7428,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2778.855,7.4,10.806,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2782.409,7.4,10.7192,ColumnAisle02_N,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,2782.359,7.35,11.1066,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2783.61,7.4,10.7225,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2783.61,7.35,11.1089,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2784.861,7.4,10.7227,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2784.861,7.35,11.109,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2786.113,7.4,10.7227,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2786.113,7.35,11.109,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2789.216,7.35,11.1487,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2790.467,7.35,11.1482,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2791.718,7.35,11.1482,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,2788.064,7.4,10.975,ColumnAisle02_N,2,5.35,体を横に向けて抜けた,1,1
w100_normal_r2,0,2840.015,7.35,8.9019,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2841.266,7.4,9.3482,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2842.518,7.4,9.4006,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2843.769,7.4,9.4546,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2845.02,7.4,9.5087,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2846.271,7.4,9.563,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2847.522,7.4,9.6174,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2848.774,7.4,9.672,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2850.025,7.4,9.7267,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2851.276,7.4,9.7816,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,2852.527,7.4,9.8366,ColumnAisle02_N,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,2878.453,11.7,12.9665,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,2879.704,11.7,12.9569,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,2880.955,11.7,12.9554,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,2882.206,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,2883.458,11.7,12.9552,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3141.559,11.12,12.3192,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3142.961,11.1124,12.3327,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3144.362,11.103,12.3398,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3145.763,11.0937,12.3473,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3147.165,11.0846,12.3552,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3148.566,11.0758,12.3634,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3149.767,11.0796,12.3604,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3149.767,11.3109,12.6863,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3151.219,11.0695,12.3763,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3152.42,11.0735,12.3728,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3152.42,11.3185,12.6886,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3153.871,11.0638,12.3892,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3155.323,11.0916,12.3706,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3156.524,11.0958,12.3674,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3156.524,11.3166,12.7005,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3157.975,11.0847,12.3829,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3159.427,11.116,12.3641,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,3159.427,11.3099,12.7132,MainAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,3317.631,11.7,12.964,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3318.882,11.7,12.9565,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3320.134,11.7,12.9554,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3321.385,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3322.636,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3323.887,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3325.138,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3326.39,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3327.641,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3328.892,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3330.143,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3331.395,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3332.646,11.7,12.9637,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3333.897,11.7,12.9699,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3335.148,11.7,12.9746,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3336.399,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3337.651,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3338.902,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3340.153,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3341.404,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3342.656,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3343.907,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3345.158,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3346.409,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3347.66,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3348.912,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3350.163,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3351.414,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3352.665,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3353.917,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3355.168,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3356.419,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3357.67,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3358.921,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3360.173,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3361.424,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3362.675,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3363.926,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3365.177,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3366.429,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3367.68,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3368.931,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3370.182,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3371.434,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3372.685,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3373.936,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3375.187,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3376.438,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3377.69,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3378.941,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3380.192,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3381.443,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3382.695,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3383.946,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3385.197,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3386.448,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3387.699,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3388.951,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3390.202,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3391.453,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3392.704,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3393.956,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3395.207,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3396.458,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3397.709,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3398.96,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3400.212,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3401.463,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3402.714,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3403.965,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3405.217,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3406.468,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3407.719,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3408.97,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3410.221,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3411.473,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3412.724,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3413.975,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3415.226,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3416.478,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3417.729,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3418.98,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3420.231,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3421.482,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3422.734,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3423.985,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3425.236,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3426.487,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3427.739,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3428.99,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3430.241,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3431.492,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3432.743,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3433.995,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3435.246,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3436.497,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3437.748,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3439,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3440.251,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3441.502,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3442.753,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3444.004,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3445.256,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3446.507,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3447.758,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3449.009,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3450.26,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3451.512,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3455.616,11.7,12.8268,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3456.867,11.7,12.8508,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3458.819,11.7,12.9259,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3460.07,11.7,12.9514,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3461.321,11.7,12.9546,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3462.573,11.7,12.9551,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3463.824,11.7,12.9551,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3465.075,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3466.326,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3467.577,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3468.829,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3470.08,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3471.331,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3472.582,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3473.833,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3475.085,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3476.336,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3477.587,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3478.838,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3480.09,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3481.341,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3482.592,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3483.843,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3485.094,11.7,12.9552,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3486.346,11.7,12.9701,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3487.597,11.7,12.9657,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3488.848,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3490.099,11.7,12.9746,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3491.351,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3492.602,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3493.853,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3495.104,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3496.355,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3497.607,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3498.858,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3500.109,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,3908.007,7.3181,11.7751,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,3909.509,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,3911.01,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,3912.511,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,3914.013,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,3915.514,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,4122.935,10.25,9.9771,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,4852.723,7.35,5.169,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4853.968,7.35,5.1243,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4855.213,7.35,5.0695,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4856.458,7.35,5.0146,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4857.704,7.35,4.9596,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4858.949,7.35,4.9045,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4860.194,7.35,4.8493,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4861.439,7.35,4.794,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4862.684,7.35,4.7385,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4863.929,7.35,4.6829,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4865.174,7.35,4.6271,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4866.419,7.35,4.5728,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4867.665,7.35,4.5186,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4868.91,7.35,4.4642,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,4870.155,7.35,4.4096,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,5063.497,7.35,2.9926,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,5063.497,7.4,3.4006,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,5076.197,7.3187,11.7745,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,5077.691,7.3135,11.7736,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,5256.44,9.8525,12.1,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,5257.635,9.8517,12.1,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,5258.831,9.8517,12.1,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6109.943,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6111.188,6.5987,12.3104,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6112.433,6.5987,12.3103,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6113.678,6.5987,12.3103,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6114.923,6.5987,12.3103,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6116.168,6.5987,12.3103,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6117.414,6.5987,12.3103,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6118.659,6.5987,12.3103,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6119.904,6.5987,12.3103,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6121.149,6.5987,12.3103,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6126.478,6.4613,12.535,,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6127.922,6.6755,12.1963,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6129.167,6.4217,12.5091,,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,6129.118,6.6796,12.2048,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6149.587,7.3143,11.7744,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6151.082,7.3132,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6152.576,7.3132,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6154.07,7.3132,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6155.564,7.3132,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6157.058,7.3132,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6158.552,7.3132,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6160.046,7.3132,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6161.541,7.3132,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6163.035,7.3132,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6164.529,7.3132,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6172.049,12.1703,0.6822,,7,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6268.073,6.8494,12.062,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6289.041,6.5989,12.3091,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6290.335,6.5989,12.3088,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6291.63,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6292.925,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6294.22,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6295.515,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6296.81,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6298.105,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6299.4,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6300.695,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6301.99,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6303.285,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6304.281,7.35,8.1057,ColumnAisle02_N,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6305.526,7.35,8.1014,ColumnAisle02_N,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6306.771,7.35,8.1014,ColumnAisle02_N,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6308.016,7.35,8.1014,ColumnAisle02_N,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6309.261,7.35,8.1014,ColumnAisle02_N,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6309.66,6.6001,12.2248,NorthAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6309.61,6.771,11.879,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6310.506,7.35,8.1014,ColumnAisle02_N,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6310.855,6.6377,12.2251,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6310.855,6.7776,11.8724,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6311.751,7.35,8.1014,ColumnAisle02_N,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6312.1,6.7797,11.8703,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6312.997,7.35,8.1014,ColumnAisle02_N,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6313.445,6.5662,12.2061,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6313.445,6.7724,11.8776,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6315.437,6.773,11.877,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6316.732,6.5688,12.2078,NorthAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6316.682,6.7727,11.8773,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6317.977,6.773,11.877,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6319.521,6.6452,12.2365,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6319.521,6.7729,11.8771,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6320.766,6.7789,11.8712,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6322.758,6.7729,11.8771,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6324.003,6.7729,11.8771,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6326.245,7.35,9.6921,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6327.44,7.35,9.6946,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6327.44,7.4,10.0898,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6328.635,7.35,9.6985,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6328.635,7.4,10.091,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6328.735,6.5989,12.3092,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6329.831,7.35,9.7031,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6329.831,7.4,10.0957,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6330.03,6.5989,12.3089,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6331.026,7.35,9.7078,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6331.026,7.4,10.1004,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6332.221,7.35,9.7125,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6332.221,7.4,10.1051,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6333.417,7.35,9.7153,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6333.417,7.4,10.1105,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6334.612,7.35,9.7189,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6334.612,7.4,10.1115,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6335.807,7.35,9.7237,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6335.807,7.4,10.1163,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6336.106,6.8482,12.0607,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6337.002,7.35,9.7285,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6337.002,7.4,10.121,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6337.5,6.8316,12.0406,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6338.198,7.35,9.7333,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6338.198,7.4,10.1258,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6338.696,6.8415,12.053,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6339.393,7.35,9.7391,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6339.393,7.4,10.1317,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6340.09,6.8277,12.0356,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6340.588,7.35,9.746,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6340.588,7.4,10.1385,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6341.784,7.35,9.7511,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6341.784,7.4,10.1446,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6342.979,7.4,10.1497,ColumnAisle02_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6342.979,7.35,9.7562,ColumnAisle02_N,2,1.1,体を横に向けて抜けた,0,0
w100_normal_r2,0,6347.412,7.3788,10.3487,ColumnAisle02_N,3,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6348.607,7.3991,10.3238,ColumnAisle02_N,3,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6348.607,7.35,9.9393,ColumnAisle02_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6349.802,7.3992,10.2921,ColumnAisle02_N,3,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6349.802,7.35,9.9061,ColumnAisle02_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6351.595,7.4,9.4999,ColumnAisle02_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6353.587,7.3506,9.0338,ColumnAisle02_N,3,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6353.587,7.4,9.4196,ColumnAisle02_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6355.58,7.352,8.9545,ColumnAisle02_N,3,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6355.58,7.4,9.3405,ColumnAisle02_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6357.373,7.395,10.03,ColumnAisle02_N,3,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6357.422,7.35,9.6411,ColumnAisle02_N,4,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,6357.472,7.4,9.2647,ColumnAisle02_N,4,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,6361.257,7.3504,9.4009,ColumnAisle02_N,3,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6361.257,7.4,9.786,ColumnAisle02_N,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6362.552,7.3506,9.3598,ColumnAisle02_N,3,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6446.872,13.3585,0.261,,8,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6536.62,11.7,12.7925,MainAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6538.263,11.7,12.7817,MainAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6539.907,11.7,12.7814,MainAisle,2,1.1,体を横に向けて抜けた,1,2
w100_normal_r2,0,6541.6,11.7,12.781,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6543.293,11.7,12.781,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6544.788,11.7,12.7942,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6546.033,11.7,12.776,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6547.278,11.7,12.7821,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6548.523,11.7,12.781,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6549.768,11.7,12.7835,MainAisle,2,1.15,体を横に向けて抜けた,1,1
w100_normal_r2,0,6551.262,11.7,12.7813,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6552.507,11.7,12.7753,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6553.752,11.7,12.7836,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6554.998,11.7,12.7813,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6556.243,11.7,12.7816,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6557.488,11.7,12.7837,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6558.733,11.7,12.7826,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6559.978,11.7,12.7827,MainAisle,2,1.15,体を横に向けて抜けた,1,1
w100_normal_r2,0,6561.472,11.7,12.7806,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6562.717,11.7,12.7747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6563.962,11.7,12.7811,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6565.208,11.7,12.7801,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6566.453,11.7,12.7827,MainAisle,2,1.15,体を横に向けて抜けた,1,1
w100_normal_r2,0,6567.947,11.7,12.7807,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6569.192,11.7,12.7748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6570.437,11.7,12.7834,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6571.682,11.7,12.7808,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6572.927,11.7,12.7834,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6574.172,11.7,12.7831,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6575.417,11.7,12.7806,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6576.663,11.7,12.7832,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6577.908,11.7,12.783,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6579.153,11.7,12.7805,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6580.398,11.7,12.7831,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6581.643,11.7,12.7829,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6582.888,11.7,12.7804,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6584.133,11.7,12.7831,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6585.378,11.7,12.7829,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6586.624,11.7,12.7804,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6587.869,11.7,12.783,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6589.114,11.7,12.7828,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6590.359,11.7,12.7803,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6591.604,11.7,12.783,MainAisle,2,1.15,体を横に向けて抜けた,1,1
w100_normal_r2,0,6593.098,11.7,12.7808,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6594.343,11.7,12.7745,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6595.588,11.7,12.7792,MainAisle,2,1.15,体を横に向けて抜けた,1,1
w100_normal_r2,0,6596.883,11.7,12.7839,MainAisle,2,1.15,体を横に向けて抜けた,1,1
w100_normal_r2,0,6598.377,11.7,12.7816,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6599.623,11.7,12.7753,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6600.868,11.7,12.782,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6602.113,11.7,12.781,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6603.358,11.7,12.7836,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6604.603,11.7,12.7833,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6605.848,11.7,12.7807,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6607.093,11.7,12.7834,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6608.338,11.7,12.7832,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6609.583,11.7,12.7806,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6612.622,11.7,12.859,MainAisle,2,1.3,体を横に向けて抜けた,1,1
w100_normal_r2,0,6615.56,11.7,12.8825,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6617.502,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6617.502,11.7,12.8847,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6618.748,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6618.698,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6620.64,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6620.64,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6621.885,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6621.835,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6623.778,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6623.778,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6625.023,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6624.973,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6626.916,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6626.916,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6628.161,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6628.111,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6630.053,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6630.053,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6631.298,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6631.249,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6633.191,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6633.191,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6634.436,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6634.386,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6636.329,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6636.329,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6637.574,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6637.524,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6639.466,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6639.466,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6640.711,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6640.662,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6642.604,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6642.604,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6643.849,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6643.799,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6645.742,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6645.742,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6646.987,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6646.937,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6648.879,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6648.879,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6650.125,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6650.075,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6652.017,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6652.017,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6653.262,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6653.212,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6655.155,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6655.155,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6656.4,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6656.35,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6658.292,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6658.292,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6659.538,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6659.488,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6661.43,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6661.43,11.7,12.8846,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6662.675,11.7,12.5045,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6662.625,11.7,12.8967,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6669.1,11.7,12.4925,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6669.1,11.7,12.8847,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6671.042,11.7,12.4923,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6671.042,11.7,12.8845,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6672.288,11.7,12.5045,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6672.288,11.7,12.8961,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6674.23,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6674.23,11.7,12.8847,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6676.073,11.7,12.4923,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6676.073,11.7,12.8845,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6677.318,11.7,12.5045,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6677.318,11.7,12.8961,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6679.26,11.7,12.4924,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6679.26,11.7,12.8847,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6680.456,11.7,12.5019,MainAisle,2,1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6680.406,11.7,12.8935,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6681.651,11.7,12.5056,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6681.651,11.7,12.8971,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6682.896,11.7,12.506,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6682.896,11.7,12.8975,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6687.578,11.7,12.4922,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6687.578,11.7,12.8844,MainAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6689.52,11.7,12.9725,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6690.765,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6692.01,11.7,12.9747,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6693.255,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6694.5,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6695.746,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6696.991,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6698.236,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6699.481,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6700.726,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6701.971,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6703.216,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6704.461,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6705.707,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6706.952,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6708.197,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6709.442,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6710.687,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6711.932,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6713.177,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6714.422,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6715.667,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6716.913,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6718.158,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6719.403,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6720.648,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6721.893,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6723.138,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6724.383,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6725.628,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6726.874,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6728.119,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6729.364,11.7,12.9748,MainAisle,2,1.1,体を横に向けて抜けた,1,1
w100_normal_r2,0,6736.785,11.7,12.8656,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6737.93,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6739.076,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6740.221,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6741.367,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6742.512,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6743.658,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6744.803,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6745.949,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6747.094,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6748.24,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6749.385,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6750.531,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6751.676,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6752.822,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6753.967,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6755.113,11.7,12.8718,MainAisle,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,6962.051,7.3184,11.7751,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6963.545,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6965.04,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6966.534,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6968.028,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6969.522,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6971.016,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6972.51,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6974.004,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6975.499,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6976.993,7.3133,11.7742,NorthAisle,2,1.05,体を横に向けて抜けた,1,1
w100_normal_r2,0,6980.379,11.9468,1.3427,MainAisle,5,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,6981.525,11.8935,1.3453,MainAisle,5,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7117.143,7.4,3.856,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7118.338,7.35,3.4544,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7118.338,7.4,3.8492,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7119.534,7.35,3.4537,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7119.534,7.4,3.8488,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7120.729,7.35,3.4504,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7120.729,7.4,3.846,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7121.924,7.35,3.4559,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7121.924,7.4,3.8484,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7123.12,7.35,3.4558,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7123.12,7.4,3.8483,ColumnAisle02_S,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7173.572,3.0451,2.35,SouthAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7174.867,2.9883,2.35,SouthAisle,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7544.617,11.7,13.675,MainAisle,2,12.2,下がって抜けた,1,1
w100_normal_r2,0,7650.95,10.25,9.6221,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7652.245,10.25,9.6135,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7653.838,10.25,9.6092,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7655.133,10.25,9.6101,ColumnAisle03_N,2,1,体を横に向けて抜けた,1,2
w100_normal_r2,0,7657.175,10.25,9.6077,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7661.757,10.25,9.6073,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7663.052,10.25,9.6104,ColumnAisle03_N,2,1,体を横に向けて抜けた,1,2
w100_normal_r2,0,7664.248,10.25,9.6061,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7668.182,10.25,9.6058,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7670.125,10.25,9.6138,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7672.017,10.25,9.6109,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7675.304,10.25,9.6054,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7677.994,10.25,9.6058,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7679.886,10.25,9.6113,ColumnAisle03_N,2,1.05,体を横に向けて抜けた,1,2
w100_normal_r2,0,7681.48,10.25,9.6085,ColumnAisle03_N,2,1,体を横に向けて抜けた,1,2
w100_normal_r2,0,7909.436,4.55,9.0444,ColumnAisle01_N,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,7909.386,4.8,9.344,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7910.681,4.55,9.041,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7910.681,4.8,9.3403,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7912.026,4.55,9.0408,ColumnAisle01_N,3,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7912.026,4.8,9.3402,ColumnAisle01_N,3,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7913.321,4.55,9.0398,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7913.321,4.8,9.34,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7914.666,4.55,9.0403,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7914.666,4.8,9.3402,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7915.96,4.55,9.0395,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7915.96,4.8,9.3397,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7917.305,4.55,9.0399,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7917.305,4.8,9.3393,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7918.65,4.55,9.0399,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7918.65,4.8,9.3393,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7919.945,4.55,9.0401,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7919.945,4.8,9.3403,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7921.29,4.55,9.0399,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7921.29,4.8,9.3398,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7922.584,4.55,9.0404,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7922.584,4.8,9.3406,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7923.879,4.55,9.0412,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7923.929,4.8,9.3415,ColumnAisle01_N,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,7925.174,4.55,9.046,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7925.174,4.8,9.3455,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7926.469,4.55,9.0463,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7926.469,4.8,9.3465,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7927.814,4.55,9.0467,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7927.814,4.8,9.3461,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7929.109,4.55,9.0469,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7929.109,4.8,9.3472,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7930.454,4.55,9.0473,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7930.454,4.8,9.3468,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7931.798,4.55,9.0473,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7931.798,4.8,9.3468,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7933.143,4.55,9.0473,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7933.143,4.8,9.3468,ColumnAisle01_N,2,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,7934.438,4.8,9.3465,ColumnAisle01_N,2,1,体を横に向けて抜けた,0,0
w100_normal_r2,0,8071.301,12.0625,0.3,,4,1.05,体を横に向けて抜けた,0,0
w100_normal_r2,0,8079.967,13.0837,0.3,,8,1.05,体を横に向けて抜けた,0,0
試算(SimPy)を同じ条件で回したコマンドと出力
haizen_sim.py と同じフォルダで、標準ファミレス・スタッフ2人・ロボット2台・35組/時・料理を運ぶだけ・最大3卓・壊れ物の使い分けなしを、40回の平均で回しました(simpy 4.1.1・Python 3)。
$ python3 -c "import haizen_sim as h, json; r=h.run_condition({'store':'family','staff':2,'robots':2,'arrivals_per_h':35,'mode':'serve','trays':3,'fragile_by':'any'}); print(json.dumps(r, ensure_ascii=False))"
{"store": "family", "staff": 2, "robots": 2, "arrivals_per_h": 35, "mode": "serve", "trays": 3, "fragile_by": "any", "dish_wait_mean": 0.6654, "dish_wait_p90": 1.2106, "cold_share": 0.0011, "seat_wait_mean": 13.9762, "turn_mean": 33.6121, "walk_km_per_staff": 3.7683, "staff_util": 0.8885, "robot_util": 0.453, "robot_share": 0.8835, "robot_bus_share": 0.0, "collects": 0, "rescues": 1.2, "pauses": 42.175, "fragile_robot": 32.575, "groups": 69.45}
仮の値を1つずつ振った確かめ(simulation/sensitivity/)
README.md
# 仮の数字の効き方を確かめる(2026-10-20)
LOG351 の試算(haizen_sim.py)で、仮置きの「一時停止の割合・一時停止の長さ・立ち往生の割合・ロボットの速さ」を1つずつ振り、結論が変わるかを見た。Unity で何を測るかを決めるため。
- 条件:LOG351 の表と同じ(混み具合は普通、最大3卓、デザートとドリンクもロボットが運ぶ、ロボット2台、使い方は「料理を運ぶだけ」と「食事中に集めて回る」)
- 店:標準ファミレス(3人 → 2人+2台・35組/時)、焼肉食べ放題(3人 → 2人+2台・20組/時)
- 各条件40回の平均(haizen_sim.py の REPS のまま)
- 実行:`python3 sweep.py`(haizen_sim.py と同じフォルダで。simpy 4.1.1、Python 3)。出力は sweep.json
## 結果(料理を運ぶだけ・料理ができてから席に届くまで)
| 振ったもの | 標準ファミレス(ロボットなし 58秒) | 焼肉食べ放題(ロボットなし 77秒) |
|---|---|---|
| 仮の値のまま | 40秒 | 59秒 |
| 一時停止の割合 0% → 90% | 38 → 45秒 | 62 → 65秒 |
| 一時停止の長さ 3〜10秒 → 30〜60秒 | 40 → 54秒 | 59 → 76秒 |
| 立ち往生の割合 1% → 20% → 30% | 40 → 52 → 58秒 | 59 → 77 → 82秒 |
| ロボットの速さ 0.5 → 0.3 m/秒 | 40 → 64秒 | 59 → 91秒 |
- 止まる回数(一時停止の割合)は、ほとんど効かない
- 効くのは、止まっている長さ・立ち往生の割合・実際に走れる速さの3つ。どれも、悪い側の値でロボットを入れた利点(料理が早く届く)がなくなる
- 入店待ちの向き(ファミレスは延びる・焼肉は縮む)は、どの値でも変わらなかった。ただし入店待ちは40回平均でも標準誤差が約1分(ファミレス 1.11分・焼肉 0.84分)あるので、2分未満の差は読まない
sweep.py
import json, haizen_sim as h
from multiprocessing import Pool
CASES={"family":dict(store="family",arrivals_per_h=35,base_staff=3),
"buffet":dict(store="buffet",arrivals_per_h=20,base_staff=3)}
def cond(case,**ov):
c=CASES[case]; return {"store":c["store"],"staff":c["base_staff"]-1,"robots":2,"arrivals_per_h":c["arrivals_per_h"],"mode":"serve","trays":3,"fragile_by":"any",**ov}
jobs=[]
for case in CASES:
c=CASES[case]
jobs.append((case,"before",{"store":c["store"],"staff":c["base_staff"],"robots":0,"arrivals_per_h":c["arrivals_per_h"],"mode":"serve","trays":1,"fragile_by":"any"}))
for mode in ["serve","patrol"]:
for s in [0,0.01,0.03,0.05,0.1,0.2,0.3]:
jobs.append((case,f"{mode}_stuck_{s}",cond(case,mode=mode,stuck_rate=s)))
for pr in [0,0.15,0.3,0.6,0.9]:
jobs.append((case,f"{mode}_pause_{pr}",cond(case,mode=mode,pause_rate=pr)))
for ps in [(3,10),(10,30),(30,60)]:
jobs.append((case,f"{mode}_pauselen_{ps[0]}-{ps[1]}",cond(case,mode=mode,pause_s=ps)))
for sp in [0.3,0.4,0.5]:
jobs.append((case,f"{mode}_speed_{sp}",cond(case,mode=mode,robot_speed=sp)))
def run(j):
case,name,c=j; r=h.run_condition(c)
return case,name,{k:r[k] for k in ["seat_wait_mean","dish_wait_mean","turn_mean","staff_util","rescues","pauses","walk_km_per_staff"]}
if __name__=="__main__":
with Pool() as p: out=p.map(run,jobs)
json.dump(out,open("sweep.json","w"),ensure_ascii=False,indent=1)
for case,name,r in out: print(case,name,round(r["seat_wait_mean"],1),round(r["dish_wait_mean"]*60),r["rescues"],round(r["pauses"],1),round(r["staff_util"],2))
sweep.json
[
[
"family",
"before",
{
"seat_wait_mean": 7.7479,
"dish_wait_mean": 0.9652,
"turn_mean": 18.8422,
"staff_util": 0.8145,
"rescues": 0,
"pauses": 0,
"walk_km_per_staff": 3.8839
}
],
[
"family",
"serve_stuck_0",
{
"seat_wait_mean": 14.1563,
"dish_wait_mean": 0.6562,
"turn_mean": 33.8956,
"staff_util": 0.8885,
"rescues": 0,
"pauses": 42.35,
"walk_km_per_staff": 3.8133
}
],
[
"family",
"serve_stuck_0.01",
{
"seat_wait_mean": 13.9762,
"dish_wait_mean": 0.6654,
"turn_mean": 33.6121,
"staff_util": 0.8885,
"rescues": 1.2,
"pauses": 42.175,
"walk_km_per_staff": 3.7683
}
],
[
"family",
"serve_stuck_0.03",
{
"seat_wait_mean": 14.9436,
"dish_wait_mean": 0.691,
"turn_mean": 34.2443,
"staff_util": 0.8893,
"rescues": 4.425,
"pauses": 42.425,
"walk_km_per_staff": 3.8263
}
],
[
"family",
"serve_stuck_0.05",
{
"seat_wait_mean": 16.3087,
"dish_wait_mean": 0.7147,
"turn_mean": 34.693,
"staff_util": 0.8906,
"rescues": 6.925,
"pauses": 42.25,
"walk_km_per_staff": 3.8799
}
],
[
"family",
"serve_stuck_0.1",
{
"seat_wait_mean": 17.1074,
"dish_wait_mean": 0.7861,
"turn_mean": 35.4968,
"staff_util": 0.8939,
"rescues": 14.725,
"pauses": 41.775,
"walk_km_per_staff": 3.9944
}
],
[
"family",
"serve_stuck_0.2",
{
"seat_wait_mean": 17.8648,
"dish_wait_mean": 0.8714,
"turn_mean": 36.1871,
"staff_util": 0.8975,
"rescues": 26.425,
"pauses": 41.9,
"walk_km_per_staff": 4.0304
}
],
[
"family",
"serve_stuck_0.3",
{
"seat_wait_mean": 16.9681,
"dish_wait_mean": 0.9596,
"turn_mean": 36.8252,
"staff_util": 0.8996,
"rescues": 39.4,
"pauses": 39.775,
"walk_km_per_staff": 4.0481
}
],
[
"family",
"serve_pause_0",
{
"seat_wait_mean": 13.1012,
"dish_wait_mean": 0.6406,
"turn_mean": 32.7124,
"staff_util": 0.8772,
"rescues": 1.375,
"pauses": 0,
"walk_km_per_staff": 3.6393
}
],
[
"family",
"serve_pause_0.15",
{
"seat_wait_mean": 12.5487,
"dish_wait_mean": 0.6606,
"turn_mean": 32.9773,
"staff_util": 0.8815,
"rescues": 1.4,
"pauses": 20.525,
"walk_km_per_staff": 3.6533
}
],
[
"family",
"serve_pause_0.3",
{
"seat_wait_mean": 13.9762,
"dish_wait_mean": 0.6654,
"turn_mean": 33.6121,
"staff_util": 0.8885,
"rescues": 1.2,
"pauses": 42.175,
"walk_km_per_staff": 3.7683
}
],
[
"family",
"serve_pause_0.6",
{
"seat_wait_mean": 16.1298,
"dish_wait_mean": 0.7102,
"turn_mean": 34.3164,
"staff_util": 0.8893,
"rescues": 1.475,
"pauses": 84.925,
"walk_km_per_staff": 3.8963
}
],
[
"family",
"serve_pause_0.9",
{
"seat_wait_mean": 13.9336,
"dish_wait_mean": 0.7549,
"turn_mean": 32.6696,
"staff_util": 0.8865,
"rescues": 1.225,
"pauses": 121.075,
"walk_km_per_staff": 3.7439
}
],
[
"family",
"serve_pauselen_3-10",
{
"seat_wait_mean": 13.9762,
"dish_wait_mean": 0.6654,
"turn_mean": 33.6121,
"staff_util": 0.8885,
"rescues": 1.2,
"pauses": 42.175,
"walk_km_per_staff": 3.7683
}
],
[
"family",
"serve_pauselen_10-30",
{
"seat_wait_mean": 15.1652,
"dish_wait_mean": 0.756,
"turn_mean": 34.06,
"staff_util": 0.8876,
"rescues": 1.6,
"pauses": 41.55,
"walk_km_per_staff": 3.8437
}
],
[
"family",
"serve_pauselen_30-60",
{
"seat_wait_mean": 13.7736,
"dish_wait_mean": 0.8996,
"turn_mean": 33.4497,
"staff_util": 0.8808,
"rescues": 1.275,
"pauses": 39.75,
"walk_km_per_staff": 3.7732
}
],
[
"family",
"serve_speed_0.3",
{
"seat_wait_mean": 16.3474,
"dish_wait_mean": 1.0651,
"turn_mean": 34.2236,
"staff_util": 0.892,
"rescues": 0.8,
"pauses": 36.325,
"walk_km_per_staff": 3.9996
}
],
[
"family",
"serve_speed_0.4",
{
"seat_wait_mean": 12.314,
"dish_wait_mean": 0.8205,
"turn_mean": 33.371,
"staff_util": 0.883,
"rescues": 1.025,
"pauses": 37.275,
"walk_km_per_staff": 3.7357
}
],
[
"family",
"serve_speed_0.5",
{
"seat_wait_mean": 13.9762,
"dish_wait_mean": 0.6654,
"turn_mean": 33.6121,
"staff_util": 0.8885,
"rescues": 1.2,
"pauses": 42.175,
"walk_km_per_staff": 3.7683
}
],
[
"family",
"patrol_stuck_0",
{
"seat_wait_mean": 11.4942,
"dish_wait_mean": 0.8381,
"turn_mean": 23.0753,
"staff_util": 0.8312,
"rescues": 0,
"pauses": 65.675,
"walk_km_per_staff": 3.1915
}
],
[
"family",
"patrol_stuck_0.01",
{
"seat_wait_mean": 10.8295,
"dish_wait_mean": 0.8545,
"turn_mean": 23.0878,
"staff_util": 0.8345,
"rescues": 2.05,
"pauses": 67.1,
"walk_km_per_staff": 3.1555
}
],
[
"family",
"patrol_stuck_0.03",
{
"seat_wait_mean": 10.8329,
"dish_wait_mean": 0.8698,
"turn_mean": 23.5002,
"staff_util": 0.84,
"rescues": 7.2,
"pauses": 65.325,
"walk_km_per_staff": 3.1897
}
],
[
"family",
"patrol_stuck_0.05",
{
"seat_wait_mean": 11.5378,
"dish_wait_mean": 0.8823,
"turn_mean": 24.3466,
"staff_util": 0.8438,
"rescues": 11.15,
"pauses": 67.125,
"walk_km_per_staff": 3.2476
}
],
[
"family",
"patrol_stuck_0.1",
{
"seat_wait_mean": 13.181,
"dish_wait_mean": 0.9378,
"turn_mean": 26.3446,
"staff_util": 0.8515,
"rescues": 19.85,
"pauses": 66.525,
"walk_km_per_staff": 3.3448
}
],
[
"family",
"patrol_stuck_0.2",
{
"seat_wait_mean": 14.2813,
"dish_wait_mean": 1.0315,
"turn_mean": 28.2701,
"staff_util": 0.8587,
"rescues": 43.375,
"pauses": 63.3,
"walk_km_per_staff": 3.4802
}
],
[
"family",
"patrol_stuck_0.3",
{
"seat_wait_mean": 14.1346,
"dish_wait_mean": 1.1086,
"turn_mean": 30.0182,
"staff_util": 0.8628,
"rescues": 61.9,
"pauses": 61.625,
"walk_km_per_staff": 3.5751
}
],
[
"family",
"patrol_pause_0",
{
"seat_wait_mean": 10.9976,
"dish_wait_mean": 0.8301,
"turn_mean": 21.9063,
"staff_util": 0.8347,
"rescues": 2.675,
"pauses": 0,
"walk_km_per_staff": 3.1224
}
],
[
"family",
"patrol_pause_0.15",
{
"seat_wait_mean": 10.7524,
"dish_wait_mean": 0.8374,
"turn_mean": 22.6889,
"staff_util": 0.8338,
"rescues": 2.375,
"pauses": 33.675,
"walk_km_per_staff": 3.1354
}
],
[
"family",
"patrol_pause_0.3",
{
"seat_wait_mean": 10.8295,
"dish_wait_mean": 0.8545,
"turn_mean": 23.0878,
"staff_util": 0.8345,
"rescues": 2.05,
"pauses": 67.1,
"walk_km_per_staff": 3.1555
}
],
[
"family",
"patrol_pause_0.6",
{
"seat_wait_mean": 12.6456,
"dish_wait_mean": 0.8889,
"turn_mean": 23.9249,
"staff_util": 0.8491,
"rescues": 2.5,
"pauses": 131.625,
"walk_km_per_staff": 3.2607
}
],
[
"family",
"patrol_pause_0.9",
{
"seat_wait_mean": 14.3756,
"dish_wait_mean": 0.9213,
"turn_mean": 25.5694,
"staff_util": 0.852,
"rescues": 2.1,
"pauses": 199.125,
"walk_km_per_staff": 3.3403
}
],
[
"family",
"patrol_pauselen_3-10",
{
"seat_wait_mean": 10.8295,
"dish_wait_mean": 0.8545,
"turn_mean": 23.0878,
"staff_util": 0.8345,
"rescues": 2.05,
"pauses": 67.1,
"walk_km_per_staff": 3.1555
}
],
[
"family",
"patrol_pauselen_10-30",
{
"seat_wait_mean": 13.849,
"dish_wait_mean": 0.9252,
"turn_mean": 23.9925,
"staff_util": 0.8412,
"rescues": 2.325,
"pauses": 65.875,
"walk_km_per_staff": 3.313
}
],
[
"family",
"patrol_pauselen_30-60",
{
"seat_wait_mean": 12.8116,
"dish_wait_mean": 1.0474,
"turn_mean": 25.1487,
"staff_util": 0.8427,
"rescues": 2.1,
"pauses": 62.2,
"walk_km_per_staff": 3.2748
}
],
[
"family",
"patrol_speed_0.3",
{
"seat_wait_mean": 13.46,
"dish_wait_mean": 1.2063,
"turn_mean": 28.2484,
"staff_util": 0.8613,
"rescues": 1.875,
"pauses": 51.75,
"walk_km_per_staff": 3.4506
}
],
[
"family",
"patrol_speed_0.4",
{
"seat_wait_mean": 12.572,
"dish_wait_mean": 1.0078,
"turn_mean": 25.7169,
"staff_util": 0.845,
"rescues": 2,
"pauses": 59.55,
"walk_km_per_staff": 3.2979
}
],
[
"family",
"patrol_speed_0.5",
{
"seat_wait_mean": 10.8295,
"dish_wait_mean": 0.8545,
"turn_mean": 23.0878,
"staff_util": 0.8345,
"rescues": 2.05,
"pauses": 67.1,
"walk_km_per_staff": 3.1555
}
],
[
"buffet",
"before",
{
"seat_wait_mean": 8.2382,
"dish_wait_mean": 1.2822,
"turn_mean": 22.6888,
"staff_util": 0.7513,
"rescues": 0,
"pauses": 0,
"walk_km_per_staff": 6.883
}
],
[
"buffet",
"serve_stuck_0",
{
"seat_wait_mean": 4.9172,
"dish_wait_mean": 0.9832,
"turn_mean": 18.2579,
"staff_util": 0.7475,
"rescues": 0,
"pauses": 61.125,
"walk_km_per_staff": 5.328
}
],
[
"buffet",
"serve_stuck_0.01",
{
"seat_wait_mean": 4.498,
"dish_wait_mean": 0.989,
"turn_mean": 18.8509,
"staff_util": 0.7524,
"rescues": 1.9,
"pauses": 59.95,
"walk_km_per_staff": 5.3988
}
],
[
"buffet",
"serve_stuck_0.03",
{
"seat_wait_mean": 3.5902,
"dish_wait_mean": 1.0228,
"turn_mean": 18.376,
"staff_util": 0.7523,
"rescues": 6.2,
"pauses": 59.15,
"walk_km_per_staff": 5.3233
}
],
[
"buffet",
"serve_stuck_0.05",
{
"seat_wait_mean": 4.6986,
"dish_wait_mean": 1.0344,
"turn_mean": 19.4924,
"staff_util": 0.7601,
"rescues": 10.625,
"pauses": 60.65,
"walk_km_per_staff": 5.419
}
],
[
"buffet",
"serve_stuck_0.1",
{
"seat_wait_mean": 6.5697,
"dish_wait_mean": 1.1906,
"turn_mean": 23.59,
"staff_util": 0.7821,
"rescues": 20.2,
"pauses": 60.425,
"walk_km_per_staff": 5.8197
}
],
[
"buffet",
"serve_stuck_0.2",
{
"seat_wait_mean": 5.7588,
"dish_wait_mean": 1.281,
"turn_mean": 23.8495,
"staff_util": 0.7909,
"rescues": 39.425,
"pauses": 59.025,
"walk_km_per_staff": 5.7876
}
],
[
"buffet",
"serve_stuck_0.3",
{
"seat_wait_mean": 7.3633,
"dish_wait_mean": 1.3647,
"turn_mean": 26.9488,
"staff_util": 0.8007,
"rescues": 58.525,
"pauses": 57.925,
"walk_km_per_staff": 6.0597
}
],
[
"buffet",
"serve_pause_0",
{
"seat_wait_mean": 4.8659,
"dish_wait_mean": 1.0287,
"turn_mean": 19.4197,
"staff_util": 0.7642,
"rescues": 1.875,
"pauses": 0,
"walk_km_per_staff": 5.4546
}
],
[
"buffet",
"serve_pause_0.15",
{
"seat_wait_mean": 6.5197,
"dish_wait_mean": 1.0105,
"turn_mean": 18.6698,
"staff_util": 0.7531,
"rescues": 2.05,
"pauses": 31.6,
"walk_km_per_staff": 5.5212
}
],
[
"buffet",
"serve_pause_0.3",
{
"seat_wait_mean": 4.498,
"dish_wait_mean": 0.989,
"turn_mean": 18.8509,
"staff_util": 0.7524,
"rescues": 1.9,
"pauses": 59.95,
"walk_km_per_staff": 5.3988
}
],
[
"buffet",
"serve_pause_0.6",
{
"seat_wait_mean": 6.5242,
"dish_wait_mean": 1.0468,
"turn_mean": 20.4119,
"staff_util": 0.7728,
"rescues": 2.15,
"pauses": 124.325,
"walk_km_per_staff": 5.6361
}
],
[
"buffet",
"serve_pause_0.9",
{
"seat_wait_mean": 5.2897,
"dish_wait_mean": 1.0777,
"turn_mean": 20.8981,
"staff_util": 0.7675,
"rescues": 2.45,
"pauses": 181.225,
"walk_km_per_staff": 5.6096
}
],
[
"buffet",
"serve_pauselen_3-10",
{
"seat_wait_mean": 4.498,
"dish_wait_mean": 0.989,
"turn_mean": 18.8509,
"staff_util": 0.7524,
"rescues": 1.9,
"pauses": 59.95,
"walk_km_per_staff": 5.3988
}
],
[
"buffet",
"serve_pauselen_10-30",
{
"seat_wait_mean": 6.692,
"dish_wait_mean": 1.1582,
"turn_mean": 22.548,
"staff_util": 0.7828,
"rescues": 1.7,
"pauses": 61.2,
"walk_km_per_staff": 5.8609
}
],
[
"buffet",
"serve_pauselen_30-60",
{
"seat_wait_mean": 5.883,
"dish_wait_mean": 1.2665,
"turn_mean": 21.2636,
"staff_util": 0.7725,
"rescues": 2,
"pauses": 57.2,
"walk_km_per_staff": 5.7074
}
],
[
"buffet",
"serve_speed_0.3",
{
"seat_wait_mean": 6.4634,
"dish_wait_mean": 1.5095,
"turn_mean": 27.1893,
"staff_util": 0.7926,
"rescues": 1.2,
"pauses": 47.5,
"walk_km_per_staff": 6.1183
}
],
[
"buffet",
"serve_speed_0.4",
{
"seat_wait_mean": 5.411,
"dish_wait_mean": 1.2313,
"turn_mean": 21.3098,
"staff_util": 0.7687,
"rescues": 1.675,
"pauses": 56.15,
"walk_km_per_staff": 5.6967
}
],
[
"buffet",
"serve_speed_0.5",
{
"seat_wait_mean": 4.498,
"dish_wait_mean": 0.989,
"turn_mean": 18.8509,
"staff_util": 0.7524,
"rescues": 1.9,
"pauses": 59.95,
"walk_km_per_staff": 5.3988
}
],
[
"buffet",
"patrol_stuck_0",
{
"seat_wait_mean": 4.4124,
"dish_wait_mean": 1.0355,
"turn_mean": 15.5138,
"staff_util": 0.7402,
"rescues": 0,
"pauses": 73.9,
"walk_km_per_staff": 5.0146
}
],
[
"buffet",
"patrol_stuck_0.01",
{
"seat_wait_mean": 5.001,
"dish_wait_mean": 1.0582,
"turn_mean": 16.2421,
"staff_util": 0.7426,
"rescues": 2.275,
"pauses": 74.075,
"walk_km_per_staff": 5.0831
}
],
[
"buffet",
"patrol_stuck_0.03",
{
"seat_wait_mean": 4.5632,
"dish_wait_mean": 1.0763,
"turn_mean": 16.5639,
"staff_util": 0.7471,
"rescues": 7.475,
"pauses": 72.125,
"walk_km_per_staff": 5.0762
}
],
[
"buffet",
"patrol_stuck_0.05",
{
"seat_wait_mean": 5.4474,
"dish_wait_mean": 1.1285,
"turn_mean": 18.746,
"staff_util": 0.7539,
"rescues": 11.8,
"pauses": 73.3,
"walk_km_per_staff": 5.2202
}
],
[
"buffet",
"patrol_stuck_0.1",
{
"seat_wait_mean": 4.5751,
"dish_wait_mean": 1.1633,
"turn_mean": 19.243,
"staff_util": 0.7573,
"rescues": 23.95,
"pauses": 69.375,
"walk_km_per_staff": 5.1767
}
],
[
"buffet",
"patrol_stuck_0.2",
{
"seat_wait_mean": 6.5318,
"dish_wait_mean": 1.2623,
"turn_mean": 22.4144,
"staff_util": 0.7729,
"rescues": 47.2,
"pauses": 71.05,
"walk_km_per_staff": 5.4903
}
],
[
"buffet",
"patrol_stuck_0.3",
{
"seat_wait_mean": 5.9739,
"dish_wait_mean": 1.3993,
"turn_mean": 24.7097,
"staff_util": 0.795,
"rescues": 67.85,
"pauses": 67.7,
"walk_km_per_staff": 5.6658
}
],
[
"buffet",
"patrol_pause_0",
{
"seat_wait_mean": 3.1337,
"dish_wait_mean": 1.0016,
"turn_mean": 14.0433,
"staff_util": 0.7346,
"rescues": 2.3,
"pauses": 0,
"walk_km_per_staff": 4.972
}
],
[
"buffet",
"patrol_pause_0.15",
{
"seat_wait_mean": 4.7654,
"dish_wait_mean": 1.0377,
"turn_mean": 16.482,
"staff_util": 0.7425,
"rescues": 2.275,
"pauses": 36.925,
"walk_km_per_staff": 5.0313
}
],
[
"buffet",
"patrol_pause_0.3",
{
"seat_wait_mean": 5.001,
"dish_wait_mean": 1.0582,
"turn_mean": 16.2421,
"staff_util": 0.7426,
"rescues": 2.275,
"pauses": 74.075,
"walk_km_per_staff": 5.0831
}
],
[
"buffet",
"patrol_pause_0.6",
{
"seat_wait_mean": 3.957,
"dish_wait_mean": 1.0518,
"turn_mean": 16.6361,
"staff_util": 0.7433,
"rescues": 2.475,
"pauses": 147.275,
"walk_km_per_staff": 5.0122
}
],
[
"buffet",
"patrol_pause_0.9",
{
"seat_wait_mean": 3.8557,
"dish_wait_mean": 1.0519,
"turn_mean": 16.5949,
"staff_util": 0.7351,
"rescues": 2.25,
"pauses": 216.525,
"walk_km_per_staff": 4.9923
}
],
[
"buffet",
"patrol_pauselen_3-10",
{
"seat_wait_mean": 5.001,
"dish_wait_mean": 1.0582,
"turn_mean": 16.2421,
"staff_util": 0.7426,
"rescues": 2.275,
"pauses": 74.075,
"walk_km_per_staff": 5.0831
}
],
[
"buffet",
"patrol_pauselen_10-30",
{
"seat_wait_mean": 5.1684,
"dish_wait_mean": 1.1478,
"turn_mean": 17.5719,
"staff_util": 0.7497,
"rescues": 2.625,
"pauses": 71.025,
"walk_km_per_staff": 5.1688
}
],
[
"buffet",
"patrol_pauselen_30-60",
{
"seat_wait_mean": 6.1565,
"dish_wait_mean": 1.2255,
"turn_mean": 18.9304,
"staff_util": 0.7575,
"rescues": 1.95,
"pauses": 70.375,
"walk_km_per_staff": 5.3632
}
],
[
"buffet",
"patrol_speed_0.3",
{
"seat_wait_mean": 6.8738,
"dish_wait_mean": 1.4361,
"turn_mean": 22.9738,
"staff_util": 0.7783,
"rescues": 1.525,
"pauses": 56.8,
"walk_km_per_staff": 5.7672
}
],
[
"buffet",
"patrol_speed_0.4",
{
"seat_wait_mean": 6.1478,
"dish_wait_mean": 1.224,
"turn_mean": 19.9971,
"staff_util": 0.761,
"rescues": 2.3,
"pauses": 67.075,
"walk_km_per_staff": 5.4578
}
],
[
"buffet",
"patrol_speed_0.5",
{
"seat_wait_mean": 5.001,
"dish_wait_mean": 1.0582,
"turn_mean": 16.2421,
"staff_util": 0.7426,
"rescues": 2.275,
"pauses": 74.075,
"walk_km_per_staff": 5.0831
}
]
]
haizen_sim.py(試算の本体・LOG351 のコード)
"""
サービスロボット導入シミュレーション #1 配膳ロボット(試作 v2)
飲食店のピーク時間を SimPy で再現する。
店のタイプ(5種)、ロボットの使い方(3種)、1往復で回る卓の数(段の数)、
倒れやすい物(デザート・ドリンク)を誰が運ぶか、を切り替えて比べられる。
数字はすべて仮置き。公開事例・メーカー仕様・観察で順に置き換える。
SimPy が扱うのは「店の図面がなくても答えられる問い」(時間・順番・数)だけ。
通路での鉢合わせ・接触・揺れ・こぼれは Unity で扱い、その結果を
「一時停止」「立ち往生」の数字や、ロボットに載せる皿の決まりとしてここに入れる。
ロボットの使い方
serve : 上げ膳だけ。下げ膳は人がやる(ガスト型)
bus : 下げ膳にも使う。皿はスタッフがロボットに載せる(サイゼリヤ・ブロンコビリー型)
patrol : 食事中に席を回り、客が空いた皿を載せる(焼肉の和民・かみむら牧場型)
使い方:
pip install simpy
python haizen_sim.py # 総当たりを回して results.json を書く
python haizen_sim.py --main # よく使う人数・台数(0〜3台)だけ先に回す
途中経過は results_partial.json に保存され、止まっても続きから回る
python haizen_sim.py --one # 1条件だけ回して結果を表示
依存: Python 3.10 以降 / simpy 4.1
"""
import json
import random
import statistics
import sys
from itertools import product
from multiprocessing import Pool
import simpy
# ---------------------------------------------------------------
# 共通の前提(すべて仮)
# ---------------------------------------------------------------
COMMON = {
"staff_speed": 1.0, # スタッフが歩く速さ(m/秒)
"robot_speed": 0.5, # ロボットが店内を走る速さ(m/秒)
"guide_s": 45, # 案内(歩く時間は別)
"staff_place_s": 10, # スタッフが皿を置く
"robot_handoff_s": 25, # 客がロボットから皿を取る
"pay_s": 90, # 会計
"call_s": 30, # 呼び出し対応(歩く時間は別)
"clear_s": 40, # テーブルの皿をまとめる
"wipe_s": 40, # 卓を拭いてセットする
"load_s": 15, # まとめた皿をロボットに載せる
"guest_load_s": 20, # 客が空いた皿をロボットに載せる(巡回回収)
"pause_rate": 0.3, # 一時停止:1往復で、人が前を横切って止まる確率(自然に解ける)
"pause_s": (3, 10), # 一時停止の長さ(秒)
"stuck_rate": 0.01, # 立ち往生:1往復で、スタッフが動かしに行かないと解けない確率
"rescue_s": 15, # スタッフがロボットを動かす作業
"hop_m": 3, # 1往復で複数卓を回るとき、卓から次の卓への寄り道(m)
"fragile_cook": (2, 4, 7), # デザート・ドリンクができるまで(分)
"cold_min": 3, # これを超えて届いた皿を「待たせた皿」とする
"dish_extra_m": 0, # 洗い場までの追加距離(店のタイプで上書き)
}
# ---------------------------------------------------------------
# 店のタイプ(すべて仮)
# dist_m : 厨房(洗い場)から卓までの距離の幅
# first_dishes : 最初の注文の皿。料理ができるまで(最短, 最頻, 最長 分)
# extra_orders : 食事中の追加注文の回数(食べ放題)
# bus_trips : 下げ膳を人だけでやるときの往復回数(皿の量)
# dish_extra_m : 洗い場が厨房の受け渡し口よりさらに奥にあるときの追加距離(ホテル小倉の事例)
# fragile : 1組あたりの倒れやすい物(デザート・パフェ・ドリンク)の数の平均
# staff : 最初に見せる人数
# staff_all : 「その他」で選べる人数(総当たりはこちら)
# arrivals : 来客ペース(空いている・普通・混雑)
# ---------------------------------------------------------------
STORES = {
"small": {
"label": "小型店", "tables": 18, "dist_m": (4, 12), "peak_min": 120,
"first_dishes": [(4, 7, 11), (6, 9, 14)], "extra_orders": 0, "extra_cook": (3, 5, 8),
"eat_min": (15, 25, 40), "call_rate": 0.4, "bus_trips": 1, "fragile": 0.3,
"staff": [1, 2, 3], "staff_all": [1, 2, 3, 4, 5], "arrivals": [15, 22, 30],
},
"family": {
"label": "標準ファミレス", "tables": 35, "dist_m": (6, 22), "peak_min": 120,
"first_dishes": [(5, 8, 12), (8, 12, 18)], "extra_orders": 0, "extra_cook": (3, 5, 8),
"eat_min": (20, 30, 45), "call_rate": 0.6, "bus_trips": 2, "fragile": 0.5,
"staff": [2, 3, 4], "staff_all": [1, 2, 3, 4, 5, 6, 7], "arrivals": [25, 35, 45],
},
"large": {
"label": "大型店", "tables": 60, "dist_m": (8, 35), "peak_min": 120,
"first_dishes": [(5, 8, 12), (8, 12, 18)], "extra_orders": 0, "extra_cook": (3, 5, 8),
"eat_min": (20, 30, 45), "call_rate": 0.6, "bus_trips": 2, "fragile": 0.5,
"staff": [4, 5, 6], "staff_all": [2, 3, 4, 5, 6, 7, 8, 9, 10], "arrivals": [45, 60, 75],
},
"hotel": {
"label": "洗い場が遠い店", "tables": 30, "dist_m": (6, 20), "dish_extra_m": 30, "peak_min": 120,
"first_dishes": [(5, 8, 12), (8, 12, 18)], "extra_orders": 2, "extra_cook": (4, 6, 10),
"eat_min": (30, 45, 60), "call_rate": 0.6, "bus_trips": 2, "fragile": 0.6,
"staff": [2, 3, 4], "staff_all": [1, 2, 3, 4, 5, 6, 7], "arrivals": [16, 22, 28],
},
"buffet": {
"label": "焼肉食べ放題", "tables": 35, "dist_m": (6, 22), "peak_min": 150,
"first_dishes": [(2, 4, 8), (3, 5, 9), (4, 7, 12)], "extra_orders": 5, "extra_cook": (2, 4, 7),
"eat_min": (60, 80, 90), "call_rate": 0.8, "bus_trips": 3, "fragile": 1.5,
"staff": [2, 3, 4], "staff_all": [1, 2, 3, 4, 5, 6, 7], "arrivals": [14, 20, 26],
},
}
ROBOTS = [0, 1, 2, 3] # 最初に見せる台数
ROBOTS_ALL = list(range(7)) # 「その他」で選べる台数(総当たりはこちら)
MODES = ["serve", "bus", "patrol"]
TRAYS = [1, 3] # 1往復で回る卓の数(1卓ずつ/最大3卓)
FRAGILE_BY = ["any", "staff"] # 倒れやすい物を ロボットも運ぶ/人だけが運ぶ
REPS = 40
def tri(rng, lmh):
lo, mode, hi = lmh
return rng.triangular(lo, hi, mode)
class Store:
def __init__(self, env, rng, p):
self.env, self.rng, self.p = env, rng, p
self.tables = simpy.Resource(env, capacity=p["tables"])
self.staff = simpy.PriorityResource(env, capacity=p["staff"])
self.robots = simpy.PriorityResource(env, capacity=p["robots"]) if p["robots"] else None
self.dish_waits, self.seat_waits, self.turns = [], [], []
self.staff_walk_m = self.staff_busy_s = self.robot_busy_s = 0.0
self.robot_trips = self.staff_trips = self.rescues = self.collects = self.done = 0
self.robot_bus = self.staff_bus = 0
self.fragile_robot = self.pauses = 0
self.pass_q = [] # 受け渡し口でロボットを待っている料理
# ---- スタッフの仕事(priority 0=ロボット救出, 1=配膳・案内・会計, 2=下げ膳)
def staff_job(self, priority, walk_m, work_s):
with self.staff.request(priority=priority) as req:
yield req
t = walk_m / self.p["staff_speed"] + work_s
self.staff_walk_m += walk_m
self.staff_busy_s += t
yield self.env.timeout(t)
# ---- ロボットが途中で止まる(1往復に1回判定)
def maybe_stop(self, d, helper_waiting=False):
p, rng = self.p, self.rng
if rng.random() < p["pause_rate"]: # 一時停止:数秒で自然に解ける
self.pauses += 1
yield self.env.timeout(rng.uniform(*p["pause_s"]))
if rng.random() < p["stuck_rate"]: # 立ち往生:スタッフが動かしに行く
self.rescues += 1
if helper_waiting: # 卓でロボットを待っているスタッフがそのまま動かす
yield self.env.timeout(d / p["staff_speed"] + p["rescue_s"])
else:
yield self.env.process(self.staff_job(0, d, p["rescue_s"]))
# ---- 配膳
# 料理ができたら、ロボットとスタッフのうち先に空いた方が運ぶ(同時ならロボット)。
# ロボットが出るときは、受け渡し口で待っている料理を段の数までまとめて載せ、近い卓から回る。
def deliver(self, d, fragile=False):
env, p = self.env, self.p
rec = {"d": d, "ready": env.now, "fragile": fragile, "taken": env.event(), "arrived": env.event()}
robot_ok = self.robots is not None and not (fragile and p["fragile_by"] == "staff")
sreq = self.staff.request(priority=1)
if not robot_ok:
yield sreq
yield env.process(self._staff_carry(rec, sreq))
return
rreq = self.robots.request(priority=0)
rec["sreq"], rec["rreq"] = sreq, rreq
self.pass_q.append(rec)
yield rreq | sreq | rec["taken"]
if rec["taken"].triggered: # ほかの料理と一緒にロボットに載った
yield rec["arrived"]
return
self.pass_q.remove(rec)
if rreq.triggered: # ロボットが運ぶ
self._drop(sreq, self.staff)
batch = [rec]
for o in list(self.pass_q):
if len(batch) >= p["trays"]:
break
self.pass_q.remove(o)
self._drop(o["rreq"], self.robots)
self._drop(o["sreq"], self.staff)
o["taken"].succeed()
batch.append(o)
env.process(self._robot_run(batch, rreq))
yield rec["arrived"]
return
rreq.cancel() # スタッフが運ぶ
yield env.process(self._staff_carry(rec, sreq))
@staticmethod
def _drop(req, res):
if req.triggered:
res.release(req)
else:
req.cancel()
def _robot_run(self, batch, rreq):
env, p = self.env, self.p
start = env.now
batch.sort(key=lambda r: r["d"])
pos = 0.0
for i, rec in enumerate(batch):
leg = rec["d"] - pos + (p["hop_m"] if i else 0)
yield env.timeout(max(leg, 0) / p["robot_speed"])
if i == 0:
yield env.process(self.maybe_stop(rec["d"]))
self.dish_waits.append((env.now - rec["ready"]) / 60)
self.robot_trips += 1
self.fragile_robot += rec["fragile"]
rec["arrived"].succeed()
yield env.timeout(p["robot_handoff_s"])
pos = rec["d"]
yield env.timeout(pos / p["robot_speed"])
self.robot_busy_s += env.now - start
self.robots.release(rreq)
def _staff_carry(self, rec, sreq):
env, p = self.env, self.p
self.staff_trips += 1
go = rec["d"] / p["staff_speed"]
yield env.timeout(go)
self.dish_waits.append((env.now - rec["ready"]) / 60)
rec["arrived"].succeed()
yield env.timeout(p["staff_place_s"] + go)
self.staff_walk_m += 2 * rec["d"]
self.staff_busy_s += 2 * go + p["staff_place_s"]
self.staff.release(sreq)
# ---- 巡回回収(食事中にロボットが来て、客が空いた皿を載せる)
def collect_visit(self, d, deadline, box):
env, p = self.env, self.p
req = self.robots.request(priority=2)
timer = env.timeout(max(deadline - env.now, 0))
res = yield req | timer
if req not in res:
req.cancel()
return
start = env.now
t = d / p["robot_speed"]
back = (d + p["dish_extra_m"]) / p["robot_speed"]
yield env.timeout(t)
yield env.process(self.maybe_stop(d))
yield env.timeout(p["guest_load_s"] + back)
self.robot_busy_s += env.now - start
self.robots.release(req)
self.collects += 1
box[0] += 1
# ---- 下げ膳(客が帰ってから卓が使えるようになるまで)
def bus(self, d, collected):
env, p = self.env, self.p
start = env.now
trips = max(p["bus_trips"] - collected, 0)
if p["mode"] == "bus" and self.robots is not None and trips > 0:
rreq = self.robots.request(priority=1)
if rreq.triggered: # 空いているロボットがいれば呼ぶ
self.robot_bus += 1
arrive = env.event()
env.process(self._robot_bus_trip(d, rreq, arrive))
# スタッフは卓まで行き、皿をまとめ、ロボットが来たら載せ、卓を拭いて戻る
with self.staff.request(priority=2) as sreq:
yield sreq
s0 = env.now
yield env.timeout(d / p["staff_speed"] + p["clear_s"])
yield arrive
yield env.timeout(p["load_s"] + p["wipe_s"])
loaded_at = env.now
yield env.timeout(d / p["staff_speed"])
self.staff_walk_m += 2 * d
self.staff_busy_s += env.now - s0
arrive.loaded = loaded_at
self.turns.append((loaded_at - start) / 60)
return
rreq.cancel()
# 人だけで下げる(皿の量に応じて往復)。皿がもうなければ拭きに行くだけ
self.staff_bus += 1
walk = 2 * (d + p["dish_extra_m"]) * trips if trips > 0 else 2 * d
work = (p["clear_s"] if trips > 0 else 0) + p["wipe_s"]
yield env.process(self.staff_job(2, walk, work))
self.turns.append((env.now - start) / 60)
def _robot_bus_trip(self, d, rreq, arrive):
env, p = self.env, self.p
start = env.now
t = d / p["robot_speed"]
yield env.timeout(t)
yield env.process(self.maybe_stop(d, helper_waiting=True))
arrive.succeed()
back = (d + p["dish_extra_m"]) / p["robot_speed"]
yield env.timeout(p["load_s"] + back + 15) # 洗い場で降ろすのは厨房側
self.robot_busy_s += env.now - start
self.robots.release(rreq)
# ---- 1組の流れ
def group(self):
env, p, rng = self.env, self.p, self.rng
arrive = env.now
d = rng.uniform(*p["dist_m"])
table = self.tables.request()
yield table
self.seat_waits.append((env.now - arrive) / 60)
yield env.process(self.staff_job(1, 2 * d, p["guide_s"]))
firsts = [env.process(self._cook_then_deliver(c, d)) for c in p["first_dishes"]]
yield simpy.AllOf(env, firsts)
eat = tri(rng, p["eat_min"]) * 60
end = env.now + eat
# 食事中の追加注文・呼び出し・巡回回収
extras = []
for _ in range(p["extra_orders"]):
extras.append(env.process(self._order_later(rng.uniform(0, eat * 0.7), d, p["extra_cook"], False)))
n_fragile = int(p["fragile"]) + (rng.random() < p["fragile"] - int(p["fragile"]))
for _ in range(n_fragile):
extras.append(env.process(self._order_later(rng.uniform(0.3, 0.85) * eat, d, p["fragile_cook"], True)))
calls = sum(rng.random() < p["call_rate"] / 2 for _ in range(2))
for _ in range(calls):
env.process(self._call_later(rng.uniform(0, eat), d))
collected = [0]
if p["mode"] == "patrol" and self.robots is not None:
for k in range(p["bus_trips"]):
at = eat * (k + 1) / (p["bus_trips"] + 1)
env.process(self._collect_later(at, d, end, collected))
yield env.timeout(eat)
if extras:
yield simpy.AllOf(env, extras)
yield env.process(self.staff_job(1, 0, p["pay_s"]))
yield env.process(self.bus(d, collected[0]))
self.tables.release(table)
self.done += 1
def _cook_then_deliver(self, cook, d, fragile=False):
yield self.env.timeout(tri(self.rng, cook) * 60)
yield self.env.process(self.deliver(d, fragile))
def _order_later(self, delay, d, cook, fragile):
yield self.env.timeout(delay)
yield self.env.process(self._cook_then_deliver(cook, d, fragile))
def _call_later(self, delay, d):
yield self.env.timeout(delay)
yield self.env.process(self.staff_job(1, 2 * d, self.p["call_s"]))
def _collect_later(self, delay, d, deadline, box):
yield self.env.timeout(delay)
yield self.env.process(self.collect_visit(d, deadline, box))
def arrivals(self):
env, p, rng = self.env, self.p, self.rng
rate = p["arrivals_per_h"] / 3600
while True:
yield env.timeout(rng.expovariate(rate))
if env.now > p["peak_min"] * 60:
return
env.process(self.group())
def run_once(params, seed):
store = STORES[params["store"]]
p = {**COMMON, **store, **params}
env = simpy.Environment()
s = Store(env, random.Random(seed), p)
env.process(s.arrivals())
env.run()
end = env.now
w = s.dish_waits
return {
"dish_wait_mean": statistics.mean(w),
"dish_wait_p90": sorted(w)[int(0.9 * (len(w) - 1))],
"cold_share": sum(x > p["cold_min"] for x in w) / len(w),
"seat_wait_mean": statistics.mean(s.seat_waits),
"turn_mean": statistics.mean(s.turns) if s.turns else 0.0,
"walk_km_per_staff": s.staff_walk_m / p["staff"] / 1000,
"staff_util": s.staff_busy_s / (p["staff"] * end),
"robot_util": (s.robot_busy_s / (p["robots"] * end)) if p["robots"] else 0.0,
"robot_share": s.robot_trips / max(s.robot_trips + s.staff_trips, 1),
"robot_bus_share": s.robot_bus / max(s.robot_bus + s.staff_bus, 1),
"collects": s.collects,
"rescues": s.rescues,
"pauses": s.pauses,
"fragile_robot": s.fragile_robot,
"groups": s.done,
}
def conditions(main_only=False):
for key, st in STORES.items():
staffs = st["staff"] if main_only else st["staff_all"]
robots_list = ROBOTS if main_only else ROBOTS_ALL
for staff, robots, arr, mode, trays, fb in product(staffs, robots_list, st["arrivals"], MODES, TRAYS, FRAGILE_BY):
if robots == 0 and (mode != "serve" or trays != 1 or fb != "any"):
continue # ロボットなしなら、使い方・段の数・使い分けは関係ない
yield {"store": key, "staff": staff, "robots": robots, "arrivals_per_h": arr,
"mode": mode, "trays": trays, "fragile_by": fb}
def run_condition(params):
runs = [run_once(params, seed) for seed in range(REPS)]
agg = {k: round(statistics.mean(r[k] for r in runs), 4) for k in runs[0]}
return {**params, **agg}
def key(c):
return (c["store"], c["staff"], c["robots"], c["arrivals_per_h"], c["mode"], c["trays"], c["fragile_by"])
def model_sig(common, stores):
"""結果に効く前提だけを取り出す(人数・台数の選択肢や名前は含めない)"""
skip = {"label", "staff", "staff_all", "arrivals"}
return json.loads(json.dumps({"common": common, "reps": REPS,
"stores": {k: {a: b for a, b in v.items() if a not in skip} for k, v in stores.items()}}))
if __name__ == "__main__":
if "--one" in sys.argv:
for mode in MODES:
for trays, fb in product(TRAYS, FRAGILE_BY):
r = run_condition({"store": "family", "staff": 3, "robots": 2, "arrivals_per_h": 35,
"mode": mode, "trays": trays, "fragile_by": fb})
print(mode, trays, fb, {k: round(v, 2) for k, v in r.items() if isinstance(v, float)})
else:
conds = list(conditions(main_only="--main" in sys.argv))
sig = model_sig(COMMON, STORES)
# 前提が同じなら、前回の結果と途中経過を使い回して足りない条件だけ回す
cache = {}
for path in ("results.json", "results_partial.json"):
try:
with open(path, encoding="utf-8") as f:
old = json.load(f)
if model_sig(old["common"], old["stores"]) == sig and old.get("reps") == REPS:
cache.update({key(r): r for r in old["rows"]})
except (FileNotFoundError, KeyError, TypeError, json.JSONDecodeError):
pass
def save(path, rows):
out = {"common": COMMON, "stores": STORES, "robots": ROBOTS, "robots_all": ROBOTS_ALL,
"modes": MODES, "trays": TRAYS, "fragile_by": FRAGILE_BY, "reps": REPS, "rows": rows}
with open(path + ".tmp", "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False)
import os
os.replace(path + ".tmp", path)
todo = [c for c in conds if key(c) not in cache]
print(f"{len(conds)} conditions, {len(conds) - len(todo)} reused, {len(todo)} to run", flush=True)
with Pool() as pool:
for i, r in enumerate(pool.imap_unordered(run_condition, todo, chunksize=2), 1):
cache[key(r)] = r
if i % 100 == 0:
save("results_partial.json", list(cache.values()))
print(f" {i}/{len(todo)}", flush=True)
save("results_partial.json", list(cache.values()))
save("results.json", [cache[key(c)] for c in conds])
print(len(conds), "conditions written", flush=True)
設計書と Claude Code への依頼文の全文
設計書は Claude が書き、依頼文は設計書をもとに Claude が書いて、Claude Code に渡しました。直しの途中で Claude Code が書き足した部分(v3 の「済み」の表や、B4・B7 で分かったこと)も、そのまま載せています。
設計書 unity_design_v0.md
# 配膳ロボット:通路で何が起きるかを Unity で測る(設計 v0)
作成:2026-10-20
対象:サービスロボット導入シミュレーション(配膳ロボット)
実装:Claude Code(Unity)/設計・結果の読み込み・試算への反映:Claude
## 1. なぜ Unity でやるのか
LOG351 の試算(SimPy。待ち時間・順番・数だけを計算する道具)では、ロボットが通路で止まる・動けなくなるところを、仮の数字で入れている。店の間取りがないと、ここは計算できないため。
仮の数字を1つずつ振って確かめた(simulation/sensitivity/README.md)。分かったのは次のこと。
- ロボットが何回止まるかは、ほとんど結果を変えない。止まる割合を0%から90%にしても、料理が届くまでの時間は標準ファミレスで38秒から45秒にしか変わらない
- 結果を変えるのは3つ。1回止まったときに何秒止まっているか、人が動かしに行かないと動けなくなる割合、人をよけながら実際に何 m/秒で走れるか
- この3つが悪い側に振れると、ロボットを入れた利点がなくなる。標準ファミレスでは、ロボットなしで58秒かかる料理が、ロボット2台で40秒になる。それが、動けなくなる割合30%で58秒、止まる長さ30〜60秒で54秒、速さ0.3 m/秒で64秒になる
なので Unity では、止まる回数ではなく、この3つを通路の幅と混み具合を変えて測る。
## 2. 何を測り、試算のどこに戻すか
| Unity で測るもの | 定義 | 戻す先(haizen_sim.py の COMMON) |
|---|---|---|
| 実際に走れる速さ | 走った距離 ÷(往復にかかった時間 − 止まっていた時間) | `robot_speed` |
| 止まっていた長さ | 1回止まってから、自分で動き出すまでの秒数の分布 | `pause_s` |
| 動けなくなった割合 | 1往復のうち、60秒以上動けなかった往復の割合(人が動かしに行く扱い) | `stuck_rate` |
| 止まった割合(参考) | 1往復のうち、1回以上止まった往復の割合 | `pause_rate` |
| 急に止まったときの減速の強さ(記録だけ) | 止まる直前の減速(m/秒²)の最大値 | まだ戻さない。こぼれを扱う回の手がかりとして残す |
戻したあと、LOG351 の表(スタッフを1人減らしてロボット2台にしたとき)を回し直し、仮の数字の版と並べる。
## 3. 作る店
(2026-10-20 改訂。最初の版は同じ卓を格子に並べ、奥が行き止まりの通路が3本ある形で、実在のファミレスに近くなかった。実際のファミレスの図面を参考に、どの場所がどこに並ぶかと寸法の目安だけを借りて作り直した。図面そのものは写さず、記事・動画にも載せない)
- 店は「部品をどこに置くかの一覧」(StoreLayout の .asset)から組み立てる。新しい形は .asset を1つ足して作る。最初の格子の店は StoreLayout_Grid として比べる用に残す
- 最初の店(StoreLayout_Family):左に客席、右に厨房。厨房と客席のさかいに、奥から「料理の受け渡し口」「パントリー(水・おしぼり・カトラリー)」「下げ膳の返却口」が並ぶ。その前を太い通路(幅1.6 m)が南北に通る。下の壁に入口(風除室つき)、入口を入ったところに待合とレジ。トイレは入口の右、事務所・更衣室は厨房の下
- 席は4人掛けで35卓:左の壁ぞいのブース8、上の壁ぞいの卓6、下の壁ぞいの卓3、中央の島18(上と下に3列×3卓ずつ)。島のまわりは回れて、行き止まりがない
- ロボットは料理の受け渡し口の前から出て、太い通路を通って卓へ行く。太い通路には、パントリーに立ち寄るスタッフ・返却口へ向かうスタッフ・待合やレジ・トイレへ向かう客がいて、人とロボットが交わる場所がここに集まる
- 背中合わせの席のあいだの仕切りは、ボックス席の背もたれの高さ 1.1 m
- ドリンクバー(2026-10-21 追記・仮):壁ぞいのカウンター 長さ 3.6 m(機械4台分)×奥行き 0.7 m。前に立つ場所は奥行き 1.5 m で、同時に4人まで。5人目からはカウンターの端に3人まで並び、それも埋まっていたら席に戻る。最初の版は 1 m ほどの台1つで、ファミレスのドリンクバーとしては狭すぎた。今の外寸では、カウンターと待合の両方が下の壁ぞいに入らなかったので、店を広げて置く(35卓は保つ。前に立つ場所と並ぶ点が通路と重なるなら、ドリンクバーのところをくぼみにする)
- レジ(2026-10-21 追記):カウンターをはさんで客の側とスタッフの側を分ける。客と反対の側に、太い通路からだけ入れるスタッフの場所(奥行き 0.8 m・幅 1.2 m・仮)を作り、会計のスタッフはそこに立つ。最初の版はスタッフが客と同じ側から立っていて、待合・入口・退店する客の流れに会計のスタッフが混ざり、レジのまわりに10人以上が固まった。待合・レジ・入口を 4.8 m² の帯に詰めたことも効いていた
- 壁沿いの卓(2026-10-21 追記):4人席(壁側ベンチ2+通路側の椅子2)をやめ、2人席(壁側ベンチ1+通路側の椅子1)にした。卓と卓のあいだを仕切りでふさいでいたため、壁側の奥の席に人が入れなかった。卓のあいだに人が入れるすき間を空け、壁の長さに入るだけ並べる。全部の席に通路から人が入れることを Build Store で確かめる
- 壁ぞいのボックス席(2026-10-21 追記):ベンチの奥行き 0.55 m(座面 0.45 m+背もたれ 0.1 m)、ブース1つの幅 1.85 m。最初の版は座面が実質 0.35 m・ブースの幅 1.65 m で、4人掛けとしては狭かった
- 細い通路の幅を3段階で切り替える:0.8 m/1.0 m/1.2 m。BellaBot の通過幅は 70 cm 以上(ソフトバンクロボティクス)/約 80 cm(シャープ)、客席間の通路の目安は 60 cm 以上・スタッフがよく通る場所は 80 cm 以上(法令ではない一般的な目安)
- 細かい寸法と人の動きは unity/claude_code_request_v1_store.md
- あとで足す形:L字型、カウンター・大人数席あり
## 4. ロボット
- 実在の機種の形は使わない。直径0.5 m、高さ1.2 m の円柱に近い棚型(仮)
- 最高速度 0.6 m/秒(仮)
- 止まる決まり(仮。実機の多くは前方の距離センサーで人を見て止まる。その動きを単純にしたもの)
- 進む方向の前0.6 m 以内に人か物があれば止まる
- 前が1秒空いたら動き出す
- 止まって10秒たったら、別の道を探す
- 60秒動けなかったら「動けなくなった」と記録し、その往復を終える
- Unity の NavMeshAgent(地図の上で道を探して進む部品)の、自動でよける機能はロボットには使わない。実機のよけ方と違うため。道探しだけ使い、止まる・動き出すは上の決まりで書く
## 5. 人
(2026-10-21 改訂。最初の版は「歩く客 1/4/8人がずっと行き来する」作りで、通路を歩く客が実際よりずっと多く、卓に戻った客が通路に立つ・人どうしが押し合って動けなくなることも起きていた。どれもロボットが止まる回数と長さを大きく見せるので、客に「入ってから出るまで」の流れを持たせた)
- 客は1組ごとに:入店 → 席が空くまで待合 → スタッフの案内で卓へ → 座る → 注文・食事(ときどきドリンクバー・トイレへ行き、戻って座る)→ 1人がレジ → 全員で出る → スタッフが片付けて卓が空く
- 来客のペース・食事の時間・料理の数・ロボットとスタッフの運び分けは、試算(haizen_sim.py の family)と同じにする。ロボットは料理を頼んだ卓にだけ行く
- 試算にない値は仮:組の人数(1人20%・2人35%・3人20%・4人25%)、ドリンクバー(1人平均1.5回・20秒)、トイレ(1組0.3回・180秒)。店で観察するときに、観察シートで数える項目に足す
- スタッフ2人は、そのとき起きている仕事(案内・料理・呼び出し・レジ・片付け)を試算と同じ優先で選ぶ。手が空いたらパントリーの前で待つ
- 座らせ方(2026-10-21 追記):1〜2人の組は空いている2人席を優先し、なければ4人席。3〜4人の組は4人席だけ。試算(LOG351)には卓の大きさの区別がないので、入店待ちが試算とずれる要因の1つになる
- 人はロボットをよける(2026-10-21 追記):細い通路の中にロボットがいたら入らずに入口で待つか、5 m 以内の回り道を通る。通路の中で向かい合ったら人のほうが交差点や卓のすき間へ下がる。広い通路ではロボットから 0.3 m 以上空けてすれ違う。これを守らない不注意な人を 1 回の行動につき 2%(仮)混ぜる。最初の版では人がロボットを気にせず歩き、細い通路でロボットを両側から押す場面が出た。実際の店で人は自分からロボットに当たりに行かない(わざとか子どもくらい)
- 人どうしのすれ違い:前に進めないと体を横に向け(半径 0.20→0.12 m)、それでも抜けられなければ客のほうが下がる
- 細かい値は unity/claude_code_request_v2_guests.md
## 6. 回す条件
(2026-10-21 改訂)
- 通路の幅 3 × 来客のペース 3(空いている 25・普通 35・混雑 45 組/時。試算と同じ)× ロボットの台数(1台・2台)= 18条件
- 1回の実行は、店の中の時間で120分(試算と同じピークの長さ)。卓の80%(仮)が食事中の状態から始め、最初の15分は数えない
- 乱数の種の数は、段階2で分かる1回の実行時間を見て決める
- 時間を速めても結果が変わらないことを、段階2で確かめる(等倍・10倍・1コマ固定の3通り)
## 7. 出力
保存先:KIOXIA の `r2r2r-data/service_sim/unity/{YYYYMMDD}_{run_name}/`(MASTER の保存先テーブルに行を足してから使う)
- `trips.csv`:1往復1行。列は condition_id, aisle_width_m, crowd, robots, seed, trip_id, distance_m, total_s, stopped_s, n_stops, stop_durations_s(; 区切り), stuck(0/1), max_decel_mps2, min_clearance_m, met_robot(0/1)
- `conditions.json`:18条件の中身と、上の仮の値すべて
- `summary.json`:条件ごとの、実際に走れる速さ・止まっていた長さ(中央値と90%点)・動けなくなった割合・止まった割合
- `stops.csv`:止まった1回が1行(時刻・位置・通路・原因・秒数・どう解けたか・左右の空き)。原因の分け方は観察シート(LOG353)の選択肢にそろえる(2026-10-21 追記)
- `scripts/`:この回の Unity の C# スクリプトのコピー(データと対のコード)
## 8. 映像にする場面(記事・動画用)
Unity Recorder で、各10〜20秒。
1. 幅0.8 m の通路で、料理を持ったスタッフとロボットが向かい合う。ロボットが止まって待つ
2. 幅0.8 m の通路で、ロボット2台が向かい合う。どちらも下がれず、60秒で「動けなくなった」になる
3. 客が椅子を引いて通路をふさぐ。ロボットが別の道を探す
## 8.5 ファイルの分け方(店・ロボット・人を別々に直せるようにする)
店・ロボット・人・実験の回し方を、別々のフォルダとファイルに分ける。数字(通路の幅、止まる距離、歩く速さ、人数など)はコードに書かず、それぞれの設定ファイル(Unity の ScriptableObject。インスペクターで値を変えられるファイル)に置く。
```
Assets/ServiceSim/
├── Scenes/ServiceSim_Family.unity … 店・ロボット・人・実験を置くだけ。中身は持たない
├── Store/ … 店
│ ├── StoreLayout_Family.asset … 卓の数と並び、太い通路と細い通路の幅、厨房の位置
│ ├── StoreBuilder.cs … StoreLayout から床・壁・卓・椅子を並べる
│ └── Prefabs/(Table_Booth4, Chair, KitchenPass, Wall)
├── Robot/ … ロボット
│ ├── RobotSpec.asset … 大きさ、最高速度、止まる距離、動き出すまでの秒数、別の道を探すまでの秒数、動けなくなったとみなす秒数
│ ├── ServingRobot.prefab
│ ├── ServingRobotController.cs … 往復・止まる・動き出す・別の道を探す
│ └── FrontSensor.cs … 前に人や物があるかを見る
├── People/ … 人
│ ├── PeopleSpec.asset … 混み具合ごとの人数、歩く速さ、椅子を引く頻度とはみ出す長さ
│ ├── Staff.prefab / Guest.prefab / SeatedGuest.prefab
│ └── StaffAgent.cs / GuestAgent.cs / SeatedGuest.cs
├── Experiment/ … 実験の回し方と記録
│ ├── ExperimentPlan.asset … 回す条件(通路の幅・混み具合・台数)、乱数の種、往復の回数、出力先
│ ├── ExperimentRunner.cs … 条件を順に回す
│ └── TripLogger.cs … trips.csv・conditions.json・summary.json を書く
└── Recording/ … 映像にする3場面のカメラ位置と、場面ごとの再現の設定
```
決まり
- 1つのスクリプトは、店・ロボット・人・実験のどれか1つだけを扱う。ロボットのスクリプトの中で人を動かさない、などの混ぜ方をしない
- 数字を直すときは設定ファイル(.asset)だけを変える。コードは触らない
- 店の形を直すときは StoreLayout だけを変える。ロボットと人のファイルは触らない
- 設定ファイルの値は、実行のたびに conditions.json にそのまま書き出す(どの値で回した結果かを後から分かるように)
見た目
- 計測に使う店とロボットと人は、箱と円柱だけで作る(形の作り込みは結果に効かないため)
- 映像にする3場面は、計測と同じシーンを使い、色と照明だけ整えて Unity Recorder で撮る
## 8.6 画面に出す情報(段階4の撮影の前に作る。2026-10-21 追記)
参考:清水秀樹さんが LinkedIn に投稿した、Unity で隅田川を再現した操船シミュレーターの画面。建物も船も白い箱なのに説得力があるのは、出している数字(桁下と船の高さ、次の変針、時刻と倍速、海図)が本物だから。見た目より、画面に出す情報の選び方を借りる。
決まり:画面に出す数字は、記録(trips.csv・stops.csv)に書く数字と同じものにする。見た目のためだけの数字は出さない。
| 場所 | 出すもの |
|---|---|
| 画面の上(いつも出す) | 店の中の時刻と倍速、条件(店の形・細い通路の幅・混み具合・台数)、受け渡し口で待っている料理の数、止まった回数・止まっていた秒数の合計・動けなくなった回数 |
| ロボットの上の字幕 | 次の行動(「卓12へ」「受け渡し口へ戻る」「通路の入口で待つ:先に1台」「止まっている:前に客 0.4 m」)。色は 走っている=白/止まっている=黄/動けなくなった=赤(あと何秒で人を呼ぶか) |
| 人の上の字幕 | スタッフ・客・待合の客が今していること(「料理を運ぶ → 卓12」「トイレへ」「卓に戻る」「待っている」など)。ロボットが止まった相手が何をしていたかが画面で分かる。確認用の表示(Scene ビューだけ)は段階2のあとに先に作る(2026-10-21 追記) |
| 床の塗り分け | ロボットの体が入れる床(水色)と、人だけが入れる床(薄い黄色)を塗り分け、凡例を付ける。Unity の NavMesh の表示(ロボットの中心が行ける範囲)は、見る人に「この細い帯しか通れない」と読まれるので、人に見せる絵には使わない(2026-10-21 追記) |
| 余裕の数字 | 「通路 80 cm・ロボット 50 cm・左右に 15 cm ずつ」。椅子が引かれたら「残り 50 cm・通れない」を赤。人との一番近い距離が決めた値を下回ったら黄(値は RobotSpec) |
| 右下の全体図 | 店の見取り図に、ロボット全台・スタッフ・客の点。止まった場所に印を残して重ねる(どこで止まりやすいかの地図) |
| 視点 | ロボットの目線/真上/店全体の斜め上/止まった場所に寄る |
なぜ「余裕の数字」を先に置くか:すかいらーくは配膳ロボットを、まず通路に余裕のある店から入れた。築年数のたった店では「通路の狭さやカーペットなどによる段差といった問題もありました」(Business Insider Japan の記事での担当者の発言)。どこで何 cm 足りないかが画面で見えると、SimPy の待ち時間の数字に理由が付く。
## 9. Claude Code への依頼で守ること
- 新しいシーン `Assets/ServiceSim/ServiceSim_Family.unity` を作る。mechArm のシーンとスクリプトには触らない
- ArticulationBody は使わない(この回はアームを使わない)。既存の ArticulationBody の設定にも触らない
- NavMesh の部品(com.unity.ai.navigation)が入っているかを最初に確かめる。入っていなければ、入れる前に報告する
- 作業の区切りごとに VCS Check-in。`git clean` は使わない
- 書き出しの前に KIOXIA がつながっているかを確かめ、つながっていなければ止めて報告する。同じ名前のフォルダがあれば上書きせずに止める
- Claude Code が書いたコードは、実行する前にエイイチが読む
## 10. この回で分からないこと
- 人の動き(何人が、どれくらいの頻度で通路を歩くか)が仮なので、Unity の結果は「通路の幅と混み具合で、3つの数字がどう変わるか」の形を示すところまで。店の実際の値は、観察シート(LOG353)で測って置き換える
- 料理のこぼれは、この回では扱わない。急に止まったときの減速の強さだけ記録しておく
依頼文 v0(段階1〜4)
# Claude Code への依頼:配膳ロボットの店を Unity で作り、通路での止まり方を測る(v0)
## 最初に読むもの
設計書:/Users/shimazueiichi/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/unity/unity_design_v0.md
この依頼は設計書の実装。設計書と食い違うところがあれば、作る前に止めて報告する。
## 目的
ファミレスの通路を Unity で作り、配膳ロボットが人をよけながら往復したときの次の3つを、通路の幅と混み具合ごとに測る。
1. 実際に走れる速さ(走った距離 ÷ 止まっていなかった時間)
2. 1回止まってから自分で動き出すまでの秒数
3. 60秒以上動けなくなった往復の割合
結果は SimPy の試算(haizen_sim.py)の robot_speed・pause_s・stuck_rate に戻す。戻す作業は Claude がやる。この依頼では、測って書き出すところまで。
## 作業の場所と守ること
- Unity プロジェクト:/Users/shimazueiichi/Robotics_UX/Robotics_UX_New/
- 新しいフォルダ Assets/ServiceSim/ の中だけで作る。mechArm のシーン・スクリプト・Prefab には触らない
- ArticulationBody は使わない。既存の ArticulationBody の値(Stiffness 20000 / Damping 2000)には触らない
- 1 Unity 単位 = 1 m
- NavMesh の部品(com.unity.ai.navigation)が入っているかを最初に確かめる。入っていなければ、入れる前に止めて報告する
- 各段階の終わりに VCS Check-in する。git clean は使わない。git add . は使わず、ファイルを1つずつ指定する
- Unity エディタの操作(Play・メニューの実行)はエイイチがやる。実行するものは、エディタのメニュー(ServiceSim/…)から1クリックで動く形にする
- エイイチが実行する前にコードを読む。各段階の報告に、新しく作ったファイルと変えたファイルの一覧を付ける
## ファイルの分け方(設計書 8.5 のとおり)
```
Assets/ServiceSim/
├── Scenes/ServiceSim_Family.unity
├── Store/ StoreLayout_Family.asset, StoreBuilder.cs, Prefabs/
├── Robot/ RobotSpec.asset, ServingRobot.prefab, ServingRobotController.cs, FrontSensor.cs
├── People/ PeopleSpec.asset, Staff/Guest/SeatedGuest の prefab と .cs
├── Experiment/ ExperimentPlan.asset, ExperimentRunner.cs, TripLogger.cs
└── Recording/
```
- 1つのスクリプトは、店・ロボット・人・実験のどれか1つだけを扱う
- 数字はコードに書かず、.asset(ScriptableObject)に置く。下の「仮の値」はすべて .asset の初期値にする
## 仮の値(すべて .asset の初期値。コードに直接書かない)
店(StoreLayout_Family)
- 卓は35卓。4人掛けのボックス席
- 厨房の受け渡し口から、太い通路(幅1.5 m)が1本伸びる。そこから細い通路が枝分かれし、卓は細い通路の両側に並ぶ
- 受け渡し口から各卓までの道のりが、だいたい6〜22 m に収まる並びにする。できた並びで、35卓それぞれの道のり(NavMesh の経路の長さ)を報告する
- 細い通路の幅:0.8 / 1.0 / 1.2 m を切り替えられる
ロボット(RobotSpec)
- 直径0.5 m、高さ1.2 m の円柱。最高速度 0.6 m/秒。加速・減速 1.0 m/秒²
- 前のセンサー:進む向きの前0.6 m、幅はロボットの直径。人・ロボット・椅子が入ったら止まる
- 前が1秒空いたら動き出す
- 止まって10秒たったら、経路を引き直す
- 60秒動けなかったら「動けなくなった」と記録する。15秒待ってから(人が動かしに来た扱い)、受け渡し口へ戻して、その往復を終える
- 卓に着いたら25秒止まる(客が料理を取る時間。これは「止まった」に数えない)
- NavMeshAgent は経路を探すのにだけ使う。自動でよける機能(obstacleAvoidanceType)は切る。止まる・動き出すは ServingRobotController に書く
人(PeopleSpec)
- スタッフ2人:1.0 m/秒で、受け渡し口と卓を行き来する。ロボットとすれ違うときは人がよける(NavMeshAgent の自動でよける機能を使ってよい)
- 歩く客:0.8 m/秒で、卓とトイレ・レジ・ドリンクバーを行き来する。人数は混み具合で変える:空いている 1人/普通 4人/混雑 8人(スタッフと合わせて、通路を歩く人が3/6/10人)
- 座っている客:全卓に座っている。1卓あたり平均10分に1回、椅子を通路側へ0.3 m 引き、30秒そのままにする。引いた椅子は NavMeshObstacle(carve あり)にする
実験(ExperimentPlan)
- 条件:細い通路の幅3 × 混み具合3 × ロボットの台数(1台・2台)= 18条件
- 1条件で、ロボットの往復を(台数を合わせて)100回 × 乱数の種5つ(0〜4)
- 行き先の卓は35卓から等しい確率で選ぶ
- run_name の初期値:family_v0
## 書き出し
保存先:/Volumes/KIOXIA/r2r2r-data/service_sim/unity/{YYYYMMDD}_{run_name}/
- 書き出す前に /Volumes/KIOXIA がつながっているかを確かめる。つながっていなければ、ほかの場所に書かずに止めて報告する
- 同じ名前のフォルダがあれば、上書きせずに止める
- 中身(列名はこのとおり)
- trips.csv:1往復1行。condition_id, aisle_width_m, crowd, robots, seed, robot_id, trip_id, table_id, distance_m, total_s, stopped_s, n_stops, stop_durations_s(; 区切り), stuck(0/1), max_decel_mps2, min_clearance_m, met_robot(0/1)
- distance_m:実際に走った道のり。total_s・stopped_s には卓での25秒を入れない
- min_clearance_m:その往復で、ロボットの外周と人の外周がいちばん近づいた距離
- met_robot:その往復で、もう1台のロボットが前のセンサーに入ったら1
- conditions.json:18条件の中身と、StoreLayout・RobotSpec・PeopleSpec・ExperimentPlan の値すべて(実行したときの値をそのまま書く)
- summary.json:条件ごとに、実際に走れる速さ(中央値)、止まっていた秒数(中央値・90%点)、動けなくなった割合、1回以上止まった往復の割合、往復の数
- scripts/:Assets/ServiceSim/ の .cs をすべてコピー
## 進め方(段階ごとに止めて報告する)
1. 店・ロボット・人を作る
- メニュー ServiceSim/Build Store で、StoreLayout から店を組み立てる
- 報告:真上から見た画面と、斜め上から見た画面のスクリーンショット、35卓の道のりの一覧、作ったファイルの一覧
- スクリーンショットの置き場所:/Users/shimazueiichi/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/unity/screens/
2. 1条件だけ回す
- 条件:通路1.0 m・普通・2台・種0。Time.timeScale を1と10の両方で回す
- 報告:2つの summary を並べたもの。timeScale で数字がずれたら、どの値がどれだけずれたか
- この段階では KIOXIA ではなく、Unity プロジェクトの中の一時フォルダに書く(測定データとして残さない)
3. 18条件を回して KIOXIA に書き出す
- 報告:summary.json の中身を表にしたもの、かかった時間
4. 映像にする3場面を撮る(Unity Recorder、各10〜20秒、mp4)
- 場面1:幅0.8 m の通路で、料理を持ったスタッフとロボットが向かい合い、ロボットが止まって待つ
- 場面2:幅0.8 m の通路で、ロボット2台が向かい合い、60秒で「動けなくなった」になる
- 場面3:客が引いた椅子で通路がふさがり、ロボットが経路を引き直す
- 置き場所:/Volumes/KIOXIA/r2r2r-data/service_sim/unity/{YYYYMMDD}_{run_name}/recordings/
- 3つの場面は、決めた位置に人・椅子・ロボットを置いて再現する(偶然起きるのを待たない)。再現の設定は Recording/ に置く
各段階の報告の最後に、次の段階に進んでよいかをエイイチに聞く。
依頼文 v1(ファミレス型の店)
# Claude Code への依頼:ファミレスの店を、実際の間取りに近い形で作り直す(v1)
前の「左右振り分け型」の依頼は取り消し。こちらに差し替える。
設計書:同じフォルダの unity_design_v0.md(3. 作る店・5. 人 を更新済み)
## なぜ作り直すか
今の店は、同じ卓を格子に並べ、奥が行き止まりの通路が3本ある形で、実在のファミレスに近くない。
実際の店では、厨房・パントリー(スタッフの作業場)・料理の受け渡し口・下げ膳の返却口・入口の待合とレジ・トイレがあり、人とロボットが交わる場所はそこで決まる。それを入れた店にする。
## 1. 店の作り方(StoreLayout)
- StoreLayout を「数字から格子を組む」形から、「部品をどこに置くかの一覧」から組む形にする
- 部品の種類
- 席:壁際ブース(ベンチ)/壁沿いの卓(壁側ベンチ+通路側に椅子)/島の卓(両側に椅子)/カウンター席/大人数席。今回使わないカウンター席と大人数席も置ける作りにする
- 場所:厨房/パントリー/料理の受け渡し口/下げ膳の返却口/入口(風除室)/待合/レジ/トイレ/ドリンクバー/事務所・更衣室
- 新しい店の形は、StoreLayout の .asset を1つ足すだけで作れる(コードは触らない)
- 今の格子の店は StoreLayout_Grid.asset として残す(消さない)
## 2. 最初の店:StoreLayout_Family.asset
座標:店の左下の角を原点、x が右(東)、z が奥(北)。単位 m。下の数字は細い通路の幅 1.2 m のときの目安。正確な位置は、下の決まりを守って計算で決めてよい。
全体(目安 26 × 15 m)
- 客席:左側(x 0〜16 くらい)
- 厨房:右側の上(x 16〜26、z 4〜15)。中は通れない箱でよい
- 事務所・更衣室:右側の下(x 19〜26、z 0〜4)。通れない箱でよい
- トイレ:x 16〜19、z 0〜4。客席側(西)に扉が1つ。中は人が入れる(ロボットは入らない)
厨房と客席のさかい(x = 16 の壁ぞい)
- 料理の受け渡し口:奥の端(z = 13 くらい)。ロボットの出発点・戻り先は、この窓口の前の点
- パントリー(水・おしぼり・カトラリー):受け渡し口の手前に、壁にそったカウンター(z 7.5〜12 くらい、奥行き 0.8 m)。スタッフが立ち寄る点は、カウンターの前
- 下げ膳の返却口:パントリーのさらに手前(z = 6.5 くらい)。スタッフが下げた皿を戻す点は、この窓口の前
通路
- 太い通路:パントリーの前を、z 1〜14 にわたって南北に通る(幅 1.6 m)。ロボットの主な道
- 細い通路:島と島のあいだ、島と壁際の席のあいだ、壁沿いの卓と島のあいだ。幅は 0.8 / 1.0 / 1.2 m で切り替え。幅は「止まっている椅子の背」と「隣の席の背または壁」のあいだで測る
- 上の島と下の島のあいだの横通路:幅 1.2 m で固定。太い通路と左の壁際の通路をつなぐ
- 行き止まりをつくらない。どの島の卓にも2方向以上から行ける
席(4人掛けで35卓)
- 左の壁ぞいのブース 8卓:卓 1.4 m(x)× 0.75 m(z)、z 方向の両側にベンチ(奥行き 0.45 m)。背もたれは高さ 1.1 m
- 上の壁ぞいの卓 6卓:壁側にベンチ、通路側に椅子2脚
- 下の壁ぞいの卓 3卓:同上。入口と待合の左に並べる
- 島の卓 18卓:上の島と下の島に分け、それぞれ3列 × 3卓。卓 0.75 m(x)× 1.2 m(z)、x 方向の両側に椅子2脚ずつ。1列の中の卓どうしのすき間は 0.1 m
- 背中合わせの席のあいだの仕切りは高さ 1.1 m(StoreLayout で変えられる値)
入口まわり
- 入口:下の壁の x 12〜14 くらい。外に風除室(奥行き 1.2 m)
- 待合:入口を入ったところ(x 11〜15、z 0〜2.4 くらい)。ピーク時に待つ客が立つ場所
- レジ:待合の中、太い通路寄りに小さな台
- ドリンクバー:下の壁ぞいの卓の右(待合の左)に台
細い通路の幅を変えたときは、島と席の並びが詰まり、店の外寸もそれに合わせて変わってよい。外寸と、卓の位置の変わり方を報告に書く。
## 3. 人の動き(PeopleSpec。値はすべて仮)
- スタッフ 2人:次の仕事を確率で選んで繰り返す
- 料理の受け渡し口 → 卓(料理を運ぶ)→ 受け渡し口
- 卓 → 下げ膳の返却口(下げ膳)
- パントリーに立ち寄る(5秒)
- 待合の客を卓へ案内する
- レジ(30秒)
- 選ぶ確率は PeopleSpec の値にする(初期値は同じ割合)
- 歩く客:卓 → トイレ → 卓、卓 → ドリンクバー → 卓、卓 → レジ → 入口から出る(出た客の代わりに、入口から新しい客が案内されて入る)
- 待合に立つ客:混み具合で人数を変える(空いている 0人/普通 2人/混雑 5人)。待合の中で立ち止まっている
- 混み具合ごとの歩く客の人数は今と同じ(1/4/8人)
- 椅子を引くのは「椅子」だけ(島の卓と、壁沿いの卓の通路側)。ブースのベンチは引かない
## 4. ロボット
- 出発点・戻り先は「料理の受け渡し口の前の点」(StoreLayout から受け取る)
- 下げ膳の返却口の点も StoreLayout から受け取れるようにしておく(あとで下げ膳の使い方を試すため。今回は使わない)
- そのほかは今のまま(止まる決まり・譲り合い・動けなくなったときの扱い)。譲り合いの「区画」は、細い通路ごとに StoreLayout が決める
## 5. 確かめて報告すること
- 真上の画面(細い通路の幅 3つ)、斜め上の画面(店全体が入るよう引いた位置から1枚)、人とロボットを置いた真上の画面
- 35卓の道のりの一覧(幅 3つ)。最短・最長・平均と、LOG351 の「標準ファミレス」の 6〜22 m との比べ(合わなくても形を優先する。合わない分は試算の側で直す)
- 行き止まりがないことの確かめ方と結果(例:どの細い通路を1か所ふさいでも、島の全卓に届く)
- 店の外寸(幅 3つ)
- 新しいファイル・変えたファイルの一覧と、変えた理由
- StoreLayout_Grid に切り替えると、前と同じ道のりが出ること
## 守ること(前と同じ)
- Assets/ServiceSim/ の中だけで作る。ServiceSim の外は変えない(描画方式も変えない)
- 1つのスクリプトは、店・ロボット・人・実験のどれか1つだけを扱う。数字は .asset に置く
- チェックインはまだしない
- Unity の操作(Setup・Build Store)はエイイチがやる。終わったら、押す順番を書いて止まる
依頼文 v2(お客さんの流れ)
# Claude Code への依頼:客に「入ってから出るまで」の流れを持たせる(v2)
段階2の3つ(等倍・10倍・1コマ固定)が終わり、その報告を出してから手をつける。回している途中にスクリプトを変えない。
設計書:同じフォルダの unity_design_v0.md(5. 人・6. 回す条件 を更新済み)
## なぜ変えるか
今の客は「歩く客 1/4/8人が、卓・トイレ・ドリンクバー・レジのあいだをずっと行き来する」作りで、実際の店より通路を歩く客がずっと多い。卓に戻った客が座らずに通路に30秒立つ、細い通路で人どうしが押し合って動けなくなる、ことも起きている。どれも、ロボットが止まる回数と長さを実際より大きく見せる。
実際の客は、ほとんどの時間を座って過ごし、ときどき立ち上がるだけ。1組ごとに「入ってから出るまで」の流れを持たせる。
あわせて、試算(SimPy の haizen_sim.py、LOG351)と同じ前提で動かす。来客のペース・食事の時間・料理の数・ロボットとスタッフの運び分けは、試算のコードと同じにする。
試算のコード:
/Users/shimazueiichi/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/simulation/haizen_sim.py
(読むところ:COMMON・STORES["family"]・Store.group()・deliver()・_robot_run()・bus())
## 1. 1組の流れ
1. 入店:入口から入る。空いている卓がなければ待合で待つ(待合の客として立つ)
2. 案内:空いた卓ができたら、スタッフが待合へ迎えに行き、組と一緒に卓まで歩く(guide_s)
3. 着席:組の全員が座る。座っている人は通路から外れる(NavMeshAgent を切り、席の位置に置く)
4. 料理:着席したら注文。料理は試算と同じく first_dishes の数と作る時間で受け渡し口に出る。食事の途中のデザート・ドリンク(fragile)と呼び出し(call_rate)も試算と同じ
5. 食事:eat_min(20・30・45 分の三角分布)。食事中に、1人ずつ次の確率でドリンクバーとトイレへ行き、戻ったら座る
- ドリンクバー:1人あたり平均 1.5 回(仮)。行き先で 20 秒(仮)
- トイレ:1組あたり 0.3 回(仮)。中で 180 秒(仮)
6. 会計:1人がレジへ行く(pay_s はスタッフがレジにいる時間)。終わったら組の全員が立って入口から出る
7. 片付け:組が出たら、スタッフが卓へ行き、皿をまとめ(clear_s)、拭き(wipe_s)、皿を下げ膳の返却口へ運ぶ。終わると卓が空く
- 組の人数:1人 20%・2人 35%・3人 20%・4人 25%(仮。試算には人数がないので Unity だけの値)
- 卓は、空いている卓から選ぶ(試算と同じく卓の大きさは見ない)
- 立ち上がった人だけが通路を歩く。座っている人は通路に出ない
## 2. 料理を運ぶ(ロボットとスタッフの運び分け)
- 試算の deliver() と同じ決まりにする:料理ができたら受け渡し口で待ち、ロボットとスタッフのうち先に空いた方が運ぶ(同時ならロボット)
- ロボットは、受け渡し口で待っている料理を「段の数」までまとめて載せ、近い卓から回る(_robot_run と同じ)。段の数は ExperimentPlan の値(1 か 3。初期値 3=LOG351 の表と同じ)
- ロボットは料理を頼んだ卓にだけ行く(今の「35卓からでたらめに選ぶ」はやめる)
- 下げ膳はスタッフだけ(試算の mode = serve)。返却口の点は使うが、ロボットは下げ膳に使わない
## 3. スタッフ
- 2人(LOG351 の「3人 → 2人+ロボット2台」に合わせる)
- 仕事は、そのときに起きている仕事から選ぶ(今の「確率で選んで繰り返す」はやめる):案内、料理を運ぶ、呼び出し、レジ、片付け。仕事の優先は試算の staff_job の priority と同じ(案内・配膳・会計が先、下げ膳はあと)
- 手が空いているときはパントリーの前で待つ
## 4. 人どうしのすれ違い(前回の依頼のとおり)
1. 前に進めない(速さがほぼ 0)状態が 1 秒続いたら、その人の半径を 0.20 m から 0.12 m にする(体を横に向ける)。抜けたら戻す
2. それでも 3 秒抜けられなければ、片方が下がる:仕事中のスタッフより、客のほうが下がる。下がる先は、いちばん近い通路の交差点か太い通路
3. 人どうしの詰まりを people_jams.csv に1回1行で書く(時刻・位置・通路・人数・秒数・どう解けたか)。summary.json に回数と秒数の合計
## 5. 計測の区切りと混み具合
- 混み具合は「来客のペース」にする:空いている 25 組/時、普通 35 組/時、混雑 45 組/時(試算の STORES["family"]["arrivals"])。今の「歩く客 1/4/8人」はやめる
- 1回の実行は、店の中の時間で 120 分(試算の peak_min)
- 始まりは空の店ではなく、ピークの途中から始める:卓の 80%(仮)に、食事の途中の組を置いて始める。食事の残り時間はでたらめに決める。始めてからの最初の 15 分は計測に数えない(慣らし)。慣らしの長さと始めの埋まり方は ExperimentPlan の値
- 回す条件は、細い通路の幅 3 × 来客のペース 3 × ロボット 1台・2台 = 18 条件のまま。乱数の種の数は、段階2で分かる1回の実行時間を見てから決める(報告に、1条件あたりの実時間の見込みを書く)
## 6. 記録に足すもの
- groups.csv:1組1行。組の番号、人数、卓、入店・案内開始・着席・食事の終わり・会計・退店の時刻、待合で待った分
- trips.csv に足す列:載せた料理の数、各料理のできた時刻と届いた時刻(料理が届くまでの待ち)
- summary.json に足すもの:入店待ちの平均(試算の seat_wait_mean と同じ意味)、料理ができてから届くまでの平均(dish_wait_mean と同じ意味)、ロボットとスタッフが運んだ料理の数、人どうしの詰まりの回数と秒数
- 試算と同じ意味の数字には、summary.json の名前も試算と同じ名前を付ける
## 7. 頭の上の表示(控えてある ServiceSimLabels.cs)
客の作り直しに合わせて文言を直してから入れる。客は「入店」「待合で待っている」「案内されて卓へ」「座っている」(座っている人は既定では出さない)「ドリンクバーへ」「トイレへ」「卓に戻る」「レジへ」「店を出る」。スタッフは「案内 → 卓5」「料理を運ぶ → 卓12」「呼び出し → 卓8」「レジ」「片付け → 卓3」「返却口へ」「パントリーで待つ」。
## 8. 確かめて報告すること
- 段階2と同じ条件(細い通路 1.0 m・普通(35 組/時)・ロボット2台・種0)を、1コマ固定の回し方で1回回す
- 直す前(段階2の1コマ固定)と直したあとで並べる:ロボットの止まった原因の内訳と秒数、止まっていた秒数の中央値と90%点、人どうしの詰まりの回数と秒数、通路を歩いていた人の数の平均
- 試算(haizen_sim.py の family・staff 2・robots 2・arrivals 35・serve・trays 3・fragile_by any)の入店待ちと料理が届くまでの待ちと、Unity の同じ数字を並べる(違って当然。違いの大きさを見る)
- 1条件あたりの実時間
- 新しいファイル・変えたファイルの一覧と理由
## 守ること(前と同じ)
- Assets/ServiceSim/ の中だけ。ServiceSim の外は変えない
- 1つのスクリプトは、店・ロボット・人・実験のどれか1つだけを扱う。数字は .asset に置く(仮の値はすべて .asset の初期値)
- ロボットの止まる決まり(前のセンサー・譲り合い・動けなくなったときの扱い)は変えない。変えるのは行き先の決め方と段の数だけ
- チェックインは、確かめた結果を見て、エイイチが良いと言ってから
- Unity の操作はエイイチがやる。押す順番を書いて止まる
依頼文 v3(1回目のあとの直し)
# Claude Code への依頼:v2 の1回目のあとの直し(v3)
回している途中にスクリプトを変えない。
設計書:同じフォルダの unity_design_v0.md
## 0. いまの状態(2026-10-21)
### もう直したもの(Run One が動かなかったので先に直した)
- `ExperimentRunner._queue` に `[SerializeField]` を付けた。Play に入るときの script の読み直しで、メニューが入れた条件が消えて「回す条件がありません」で止まっていた。Run One・Run All・検査1・検査2 がすべてこの経路を通る
- 往復の記録のバグを **案A** で直した。走り始めた時刻を `ServingRobotController.RunStartS` としてロボット自身が持ち、`ExperimentRunner.OnRunDone` がそれを読む。`ExperimentRunner` 側で覚えていた `_runStartS` は外した。処理の順番(`ops.Advance` の位置)は変えていない
- 何が起きていたか:走り終わったロボットに `AssignRobots()` がすぐ次の仕事を入れるので、「空いている」状態が一度も観測されず、`_runStartS` が慣らし中の値のまま固まる。その結果 `startS < _measureStartS` で **計測窓の往復が100%捨てられ**、trips.csv と stops.csv が0行になっていた
### A の報告:済み(2026-10-21)
v2 の1回目は計測に使わない。分かったことだけ残す。
- 卓が回らない:待ち組が 0→4→7→10→16→20→…→30 と単調に増え、120 分でほぼ回らなかった。`turn_mean` 63.0 分・`seat_wait_mean` 48.6 分
- 原因は「あきらめる道」が無いこと。`GuestAgent` に時間切れが無く、用事から戻れない客がいると `activeErrands > 0` のままで組が `Eating` から出られない。`ServiceOps.AbandonJob()` は定義だけで呼び出し元が1つも無い。あきらめる仕組みを持つのはロボットだけ(`maxTripSec` 600 秒)→ **B1 で直す**
- 皿:届いた 19・人が運んだ 51・**戻した 369** → B8 で調べる
- 人どうしの詰まり 2,898 回・合計 3,075 秒 → B9 で調べる
- 倍速は 8.16 倍(見込みの 24 倍ではない)。1回 16.5 分。走行中に 6.0 倍〜13.4 倍まで変わる(歩く体の数しだい)
- 椅子を引く動き:卓ごとに見えない `SeatedGuest` が1つ(当たり判定も見た目も NavMeshAgent も無い)。引けるのは 27/35 卓・90 脚(ブース8卓はベンチなので引かない)。**座る・立つとは連動しておらず**、占有卓あたり平均 600 秒に1回の指数分布タイマーで、0.3 m 引いて 30 秒そのまま → **B6 で直す**
- 停止点:`deliveryOffsetFromAisleCenter` を 0.15 → 0(通路の中央)に変えた。通路 1.0 m・ロボット直径 0.5 m だと、0.15 のときは停止点が引かれた椅子に 0.20 m 食い込み、0 だと 0.05 m(`arriveRadius` 0.25 で避けられる)
- 段階2の出力は KIOXIA へコピー済み。`20261021_stage2_family_v0/` の直下に ts1・ts10・fixed・fixed_BEFORE_v2 の4つ。全20ファイルをバイト単位で照合して一致。`fixed_BEFORE_v2` は `fixed` と中身が同じで、別の実行ではない
### 進め方
B から始めてよい。**検査2 の前に B を入れる**。
### B の実装:済み(2026-10-21、コンパイル確認済み)
| | 内容 | 状態 |
|---|---|---|
| B1 | あきらめて、やり直す道。give_ups.csv | 済み |
| B2 | ロボットの待機場所 | 済み |
| B3 | 慣らしを食事の途中らしく混ぜる | 済み |
| B4 | ドリンクバーを広げる | **仕組みは済み。寸法が入らない(下記)** |
| B5 | 字幕を見やすくする(5項目) | 済み |
| B6 | 椅子を人の動きに結びつける+回数を記録 | 済み |
| B7 | 椅子が戻っても 60 秒動けない件 | **調べる記録だけ済み。直しは未(下記)** |
| B8 | 戻した皿の内訳。dishes.csv | 済み(報告は C1 の出力から) |
| B9 | 詰まりのうちロボットが原因のもの | 済み |
| B10 | 人の1コマの移動量 | 済み |
| B11 | 計測中は字幕を自動で切る | 済み |
あわせて入れたもの:
- 進捗の行に「走り N 回・記録 N 行」を並べ、計測が始まってから3回走っても
trips.csv が0行なら、その場で止めて Console に赤で知らせる
- `Setup` が、.asset に書かれていない項目をいまの初期値で書きそろえる
(すでにある値は上書きしない)。何を書き足したかを Console に出す
- `ExperimentPlan.disableRenderingWhileRunning` の初期値を ON にした
#### B4 で分かったこと:3.6 m のカウンターは今の店に入らない
入口まわりの帯(南の壁ぞい)の寸法:
| 細い通路 | 南の間口 | 壁卓3つ | 残り(ドリンクバー+待合+レジ) |
|---|---|---|---|
| 0.8 m | 7.35 m | 3.68 m | **3.68 m** |
| 1.0 m | 7.95 m | 3.98 m | **3.97 m** |
| 1.2 m | 8.55 m | 4.28 m | **4.27 m** |
- 長さ:3.6 m のカウンターを置くと、待合とレジに残るのは 0.37 m(通路 1.0 m のとき)。待合には最大8組を立たせるので足りない
- 奥行き:カウンター 0.7 m + 立つ場所 1.5 m = 2.2 m 必要だが、南の帯は 1.65 m しかない。はみ出した分は南の細い通路をふさぐ
- 壁ぞいの置き場所の総量:北 7.95 m + 南 7.95 m = 15.9 m。壁卓9つで 11.93 m を使うので残りは 3.98 m。ここに 3.6 m のカウンターと待合(約 2.8 m)の両方は入らない
- **つまり、35卓を保ったまま 3.6 m のカウンターを置ける並べ方が、今の外寸には無い**
いま入れたのは「仕組み」まで。長さ・奥行き・立つ点の数・並ぶ点の数は
すべて StoreLayout の .asset の値で効き、帯に入りきらない分は切り詰める。
`drinkBarSize` は今までの 1.2 × 0.7 のまま置いてある。
**どれにするか決めてほしい:**
1. 店を広げる:南(または北)の帯を 1.65 m → 2.2 m に深くし、外寸を 0.55 m 伸ばす。
長さは壁ぞいの残り 3.98 m に収まる範囲(機械3台・2.7 m 程度)にする
2. 機械を減らす:2台(1.8 m)にして今の帯に収める。35卓も外寸もそのまま
3. 卓を動かす:壁卓を減らして島か別の帯に移す。35卓は保つが、島の帯に空きが無いので
外寸を伸ばすことになる(1 とほぼ同じ)
決まれば、`drinkBarSize` と `drinkBarStandCount` を .asset で変え、
35卓の道のりと行き止まりの確かめを取り直す。
#### B7 で分かったこと:直しは「止まる決まりを変えない」に触るので止めた
守ることに「ロボットの止まる決まり(前のセンサー・譲り合い・動けなくなったときの扱い)は
変えない」とあるので、直しは入れずに**原因が分かる記録だけ**入れた。
stops.csv に、止まっている間の内訳を秒数で足した:
- `blocked_s`:前のセンサーが「ふさがっている」と言っていた秒数
- `no_path_s`:行き先への道が引けていなかった秒数
- `yield_s`:細い通路の入口で順番を待っていた秒数
- `clear_wait_s`:ふさがりが解けたあと、動き出すまで待っていた秒数
- `replans`:止まっている間に経路を引き直した回数
これで「椅子が戻ったのに動けない 30 秒」が、どれで埋まっていたかが分かる。
コードを読んだ時点での見立て(C1 の出力で確かめる):
`hasPath` は `_corners.Length > 0 && _cornerIndex < _corners.Length` で決まる。
停止点が引かれた椅子の carve に入ると `NavMesh.CalculatePath` が
`PathPartial`(途中まで)を返し、corners はあるので `hasPath` は true のまま走り出す。
その途中までの経路を使い切ると `_cornerIndex` が端に達して `hasPath` が false になり、
`wantMove` が立たなくなる。`path_blocked` は止まり始めの値なので 0 のまま。
→ `no_path_s` が 30 秒を超えていれば、この見立てが当たり。
直す案(決めてほしい):止まっていて `no_path_s` が `replanAfterStoppedSec` を超えたら、
引き直しの間隔(`repathRetrySec`)を無視して強く引き直す。それでも途中までなら、
いちど通路の入口(`aisleEntrance`)を目指して出る。
これは「動けなくなったときの扱い」に触るので、エイイチの許可が要る。
## B. 直すこと
### B1. あきらめて、やり直す道(値はすべて .asset の仮の値)
- 客:ドリンクバー・トイレへ向かって 60 秒たっても着けなければ、あきらめて席に戻る。席に戻るのにも 60 秒たって着けなければ、席の位置へ移して座らせる
- スタッフ:卓・レジ・返却口・受け渡し口へ向かって 60 秒たっても着けなければ、その仕事を板に戻し(AbandonJob を使う)、別の仕事を取る。同じ仕事を3回あきらめたら、その場所へ移して仕事を済ませる
- 組:会計・片付けで止まって卓が空かない状態が 10 分続いたら、卓を空ける
- あきらめた・移した・卓を空けたことは、すべて give_ups.csv に1回1行で書く(時刻・誰が・何の仕事で・どこへ向かっていたか・その時の位置と通路・何秒かかったか・あきらめた/移した/卓を空けた)。summary.json に回数を出す
- 「移した」「卓を空けた」は、人が通れない店になっているしるし。回数が多ければ、店か人の動きの作りを直す(計測として信用できるかの目安にする)
### B2. ロボットの待機場所
- ロボットが仕事のないときに止まる場所を、受け渡し口の前の点とは別に作る(受け渡し口の横、太い通路をふさがない壁ぎわに2台分)
- 料理を載せるときだけ受け渡し口の前の点へ行き、載せたら出る。戻ってきたら待機場所へ下がる
- 受け渡し口の前の点にはスタッフも料理を取りに来るので、ロボットが居座らないようにする
### B3. 始まりの店をピークの途中らしくする
- 卓の 80% に組を置くとき、座ったばかりで料理を待っている組・料理を受け取って食べている組・食べ終わりかけの組を混ぜる(食事のどの時点かを、でたらめに決める)
- 座ったばかりの組の料理は、受け渡し口の待ち行列に入れる(作る時間の残りもでたらめに決める)
- いまは `SeedWarmupGroups()` が `Seated` を飛ばして `Eating` から始めるので、慣らしで置いた組には 1皿め・2皿めが一度も作られない。ロボットの仕事がデザートだけになっている
### B4. ドリンクバーを広げる(StoreLayout の仮の値)
- カウンター:長さ 3.6 m(機械4台分)、奥行き 0.7 m。壁ぞいに置く
- 前に立つ場所:奥行き 1.5 m。立つ点を4つ(機械1台に1つ)作り、客は空いている点へ行って 20 秒立つ
- 4つとも埋まっていたら、カウンターの端の「並ぶ点」に順に並ぶ(3つまで)。それも埋まっていたら、あきらめて席に戻る。並んだ人数と待った秒数を記録し、summary.json に出す
- 置き場所は今と同じく入口の近く。入りきらなければ別の壁ぞいへ移してよい。35卓は保つ(減らす必要があれば、どの卓を移したかを報告する)
- 広げたあと、35卓の道のりと行き止まりの確かめを取り直す
### B5. 頭の上の字幕を見やすくする(見るだけの表示。人とロボットのスクリプトは変えない)
1. 字幕に濃い半透明の帯を敷き、文字は白にする。誰の字幕かは帯の左端の色で分ける(ロボット=白、スタッフ=青、客=黄、待合=紫)。止まっている・動けないロボットは、帯の色を黄・赤にする
2. 待合の客は1つにまとめる:「待合 8人・外で待つ 12組」
3. 出す相手を切り替えられるようにする:ロボットだけ/ロボットとスタッフ/全員(メニュー ServiceSim/Show Labels の下に3つ。初期値はロボットとスタッフ)。「全員」のときも、座っている客と椅子を引いている客は、ロボットが止まった相手になったときだけ出す
4. 字を今より大きくする(Scene ビューの拡大・縮小で読める大きさを保つ)。近くの字幕どうしは上下にずらして重ならないようにする
5. 言葉を短くする(例:「[2] 停止:前にロボット」「[1] 卓29へ(3卓)」「[1] 動けない:あと6秒」「スタッフ 案内→卓05」)
### B6. 椅子を引く動きを、人の動きと結びつける
- **卓ごとのタイマーをやめる。** いまは `SeatedGuest` が占有卓あたり平均 600 秒の指数分布で、座る・立つとは関係なく引いている
- 人が動く瞬間に、その人の椅子を引く:
- 座るとき(案内されて席に着く)
- 立つとき:ドリンクバーへ行く・トイレへ行く・会計へ行く・退店する
- 戻ってきて座り直すとき
- 引いたまま秒数は .asset の仮の値(いまの `chairPullHoldSec` 30 秒をそのまま使ってよい)。引く距離も今まで通り `chairPullDistance` 0.3 m
- 引く椅子は、その人の席の椅子にする(いまのように卓の引ける椅子からでたらめに選ばない)
- 食事中に何もなく引く分は、残すなら**別の値**にして小さくする(`chairPullIdleMeanIntervalSec` のような名前で .asset に分ける)。いまの 600 秒をそのまま残さない
- **引いた回数を記録して run_info.json に出す。** いまはどこにも記録が無く、「椅子が原因の止まり」が減ったとき、停止点を中央にしたためか引く回数が減ったためかを分けられない
- 出すもの:引いた回数の合計、引いた卓ごとの回数、きっかけの内訳(座る・ドリンクバー・トイレ・会計・退店・食事中)
### B7. 椅子が戻ったのに 60 秒動けない件を調べて直す
- 段階2の「直す前」(stage2_family_v0_fixed_BEFORE_v2 の stops.csv)で、椅子が原因の止まり 17 回のうち **11 回が 60 秒の `stuckAfterStoppedSec` 上限**に達している
- 椅子は 30 秒で戻る。`replanAfterStoppedSec` は 10 秒なので、長くても 40 秒で動き出すはず。**30 秒を超えて縛られている**
- 特に見るところ:Table35/Chair、trip 8・15・19 の3行。座標 (9.948, 11.149) も左右の空き(左 234.99 cm・右 189.83 cm)もバイト単位で同じ。左右に 1.9 m 空いているのに 60 秒動けず、`path_blocked` は 0
- 何がロボットを縛っていたかを調べて直し、**報告に書く**(椅子の NavMeshObstacle の carve か、経路の引き直しか、前のセンサーか、別のものか)
### B8. 届けられずに戻した皿 369 の内訳を調べる
- v2 の1回目:届いた皿 19・人が運んだ 51 に対して、**戻した皿 369**
- 戻すのは `ServiceOps.OnRobotRunDone()` の `done = r.StopsCompleted` に届かなかったぶん
- 調べて報告する:どの走りで戻したか、なぜ戻したか(動けなくなった・`maxTripSec` で打ち切り・その他)。走り1回あたり何皿戻したか、同じ皿が何回戻されたか
- 必要なら、戻した理由を1皿1行で書き出す(dishes.csv のような形でよい)
### B9. 人どうしの詰まりのうち、ロボットが原因のものを数える
- v2 の1回目:人どうしの詰まり 2,898 回・合計 3,075 秒(people_jams.csv)
- このうち、**ロボットが卓の前で止まっていて人が通れなかった**ものが何回あるかを数え、summary.json に出す
- 背景:停止点を通路の中央にしたので、通路 1.0 m のとき停まったロボットの左右は 0.25 m ずつになる。人は半径 0.2 m なので 0.4 m 必要で、**どちらの側も通れない**。停止点が 0.15 m ずれていた頃は片側 0.40 m あって人がすり抜けられた。椅子の止まりは減っても人の詰まりが増えているかもしれないので、数で見る
### B10. 人の1コマの移動量も記録する
- いま `frames_over_005m` は `robotSteps`(ロボットだけ)を見ている(`TripLogger.cs`)
- ロボットは 0.6 m/秒 × 0.05 秒 = 0.03 m で安全。しかし**スタッフは 1.0 m/秒 × 0.05 秒 = ちょうど 0.050 m** で境目、客は 0.8 m/秒 × 0.05 秒 = 0.04 m
- 止まりの原因の多くを作る人の側が、すり抜けの確かめの対象外になっている
- 人の1コマの移動量も記録し、**検査3の「0.05 m を超えたコマ」に人も入れる**。run_info.json には、ロボットと人を分けて出す
### B11. 計測の実行中は字幕を自動で切る
- Run One・Run All・検査1・検査2 の間は、頭の上の字幕を出さない(Play を抜けたら元の設定に戻す)
- 理由:字幕は Scene ビューで毎コマ描いていて、35卓ぶん以上あると重い。計測の倍速に効く
- B5 の「出す相手の切り替え」とは別に、計測中はまとめて切る
## C. 直したあとに確かめること
### C1. Run One を2回(停止点だけを変える)
ほかはすべて同じにする:細い通路 1.0 m・35 組/時(普通)・2台・種0・1コマ 0.05 秒。
1. `deliveryOffsetFromAisleCenter = 0`(通路の中央。いまの値)
2. `deliveryOffsetFromAisleCenter = 0.15`(卓寄り。直す前と同じ値)
停止点だけが違う2回なので、椅子が原因の止まりの差は**停止点の効果そのもの**になる。
### C2. 2回を並べる
- ロボットの止まった原因の内訳と秒数(「分からない」の割合も)
- 動けなくなった割合
- **引いた椅子1回あたりの止まり**(B6 で出す引いた回数を使う)
- 人どうしの詰まりのうち、ロボットが原因のもの(B9 で出す数)
- 入店待ち(seat_wait)と、料理ができてから届くまでの待ち(dish_wait)
- あきらめた・移した回数(B1 の give_ups.csv)
### C3. 試算と並べる
`haizen_sim.py` の family・staff 2・robots 2・arrivals 35・serve・trays 3 の
`seat_wait_mean` と `dish_wait_mean` を、C1 の2回と並べる。
### C4. 段階2の「直す前」と並べる
`stage2_family_v0_fixed_BEFORE_v2` と並べる。**店の中の時間あたりに直して**比べる
(直す前は約 18 分・20 往復、v3 は 120 分なので、そのままの回数では比べられない)。
### C5. 真上・字幕ありの画面を1枚
真上から、字幕を出した画面を1枚 `unity/screens/` に置く(字幕とドリンクバーの確認用)。
計測中は字幕が自動で切れる(B11)ので、撮るのは計測を回していないとき。
### C6. 時間の見込みを更新する
- 倍速と、1回あたりの実時間
- 検査2(1.0 m・普通・2台、種5、1コマ 0.02 秒と 0.05 秒)の手順と時間の見込み
- いまの見込み(v2 の1回目の実測 8.16 倍・16.5 分から):
- 0.05 秒:1本 15〜25 分 → 種5つで 1 時間 20 分〜2 時間
- 0.02 秒:1本 38〜62 分(コマ数が 2.5 倍)→ 種5つで 3 時間 10 分〜5 時間
- 合わせて 4 時間 30 分〜7 時間
- 短くする手:`ExperimentPlan.disableRenderingWhileRunning` を ON(いまは false)、字幕を切る(B11)。効果は軽い検査1で先に測る
※ C5 は前の C にあったものを戻した(字幕とドリンクバーの確認用)。
## 直したこと(前に「決めてほしい」と書いた件)
- `.asset` に項目が書かれていなかった件は、`Setup` で書きそろえるようにした。
`Setup` を押すと、`StoreLayout_Family` / `StoreLayout_Grid` / `RobotSpec` /
`PeopleSpec` / `ExperimentPlan` / `ServiceSpec` の、まだ書かれていない項目を
いまの初期値で書き出す。すでに書かれている値は読み込んだものがそのまま残るので
上書きされない。何を書き足したかは Console に出る
- `disableRenderingWhileRunning` も .asset に出るようになり、初期値は ON
## 守ること(前と同じ)
- Assets/ServiceSim/ の中だけ。ServiceSim の外は変えない
- 1つのスクリプトは1つの関心だけを扱う。数字は .asset に置く
- ロボットの止まる決まり(前のセンサー・譲り合い・動けなくなったときの扱い)は変えない
- チェックインは、確かめた結果を見てエイイチが良いと言ってから
- Unity の操作はエイイチがやる。押す順番を書いて止まる
この回に使ったコードの全文
Mac の /Users/shimazueiichi/Robotics_UX/Robotics_UX_New/Assets/ServiceSim/ の中身です(2026-10-21 12時すぎ、区切った時点のもの。場所の名前と警告が直っていない状態)。シーンファイル(ServiceSim_Family.unity)と、使っていない古い設定(Store/old~/)は載せていません。Unity 2022.3.62f3 で動かしています。
Store/BandStoreLayout.cs
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 実際のファミレスに近い形の店。
/// 左に客席、右に厨房。厨房と客席のさかいに、奥から
/// 「料理の受け渡し口」「パントリー」「下げ膳の返却口」が並ぶ。
/// その前を太い通路が南北に通る。下の壁に入口・待合・レジ、入口の右にトイレ。
///
/// 位置は「帯(部品の並び)の足し算」で決める。細い通路の幅を変えると
/// 席の並びが詰まり、店の外寸もそれに合わせて変わる。
/// 数字はすべてこの .asset に置く。
///
/// 下の壁ぞい(南の帯)の並びは、西から東へ
/// ブースの列 → 細い通路 → 壁卓3つ → ドリンクバー → 太い通路 → 待合・レジ・入口
/// ドリンクバーは奥行きが帯に入りきらないので、そこだけ壁を南へ出して
/// くぼみ(アルコーブ)にする。待合・レジ・入口は、トイレを北へ寄せて空けた
/// 太い通路の東の帯に置く。
/// </summary>
[CreateAssetMenu(menuName = "ServiceSim/Store Layout (Band)", fileName = "StoreLayout_Family")]
public class BandStoreLayout : StoreLayout
{
[Header("通路")]
[Tooltip("太い通路の幅(m)。パントリーの前を南北に通る。ロボットの主な道")]
public float mainAisleWidth = 1.6f;
[Tooltip("上の島と下の島のあいだの横通路の幅(m)。固定")]
public float crossAisleWidth = 1.2f;
[Header("厨房とサービス(客席の東)")]
[Tooltip("厨房・事務所の奥行き(X, m)")]
public float kitchenDepthX = 10f;
[Tooltip("パントリーなどのカウンターの奥行き(X, m)")]
public float serviceStripDepth = 0.8f;
[Tooltip("トイレの大きさ(X, Z, m)。客席側(西)に扉が1つ")]
public Vector2 toiletSize = new Vector2(3.0f, 4.0f);
[Tooltip("トイレの扉の幅(Z, m)")]
public float toiletDoorWidthZ = 1.0f;
[Tooltip("料理の受け渡し口の中心が、北の壁からどれだけ手前か(m)")]
public float passFromNorthZ = 1.5f;
[Tooltip("料理の受け渡し口の窓口の長さ(Z, m)")]
public float passWindowLengthZ = 1.2f;
[Tooltip("パントリーの北端が、北の壁からどれだけ手前か(m)")]
public float pantryFromNorthZ = 2.5f;
[Tooltip("パントリーのカウンターの長さ(Z, m)")]
public float pantryLengthZ = 4.5f;
[Tooltip("下げ膳の返却口の中心が、北の壁からどれだけ手前か(m)")]
public float returnFromNorthZ = 8.0f;
[Tooltip("返却口の窓口の長さ(Z, m)")]
public float returnWindowLengthZ = 1.0f;
[Header("席:左の壁ぞいのブース")]
public int wallBoothCount = 8;
[Tooltip("ブースの卓の大きさ(X, Z, m)")]
public Vector2 boothTableSize = new Vector2(1.4f, 0.75f);
[Tooltip("ベンチの奥行き(m)。座面+背もたれの合計")]
public float benchDepth = 0.55f;
[Tooltip("ベンチの座面の高さ(m)")]
public float benchSeatHeight = 0.45f;
[Tooltip("背もたれの板の厚み(m)。ベンチの奥行きのうち、この分が背もたれ")]
public float benchBackThickness = 0.1f;
[Tooltip("背もたれの高さ(m)。背中合わせの席では、これが仕切りを兼ねる")]
public float benchHeight = 1.1f;
[Tooltip("ベンチ1本を2つの座る場所に見せるとき、まん中に空けるすき間(m。片側ぶん)。見た目だけ")]
public float benchSeatGapX = 0.05f;
[Tooltip("座る場所の見た目の板の厚み(m)。座面の上に敷くだけ。見た目だけ")]
public float benchSeatPadThickness = 0.02f;
[Header("席:上下の壁ぞいの2人席")]
[Tooltip("壁ぞいの2人席の卓の大きさ(X 壁に沿う向き, Z 奥行き, m)。壁側にベンチ1人、通路側に椅子1人")]
public Vector2 wallTableSize = new Vector2(0.7f, 0.6f);
[Tooltip("卓と卓のあいだのすき間(X, m。いちばん狭いところ)。ここを通って壁側の席に入る。" +
"焼いた NavMesh の通行半径(navAgentRadius)の2倍より広くしないと人が通れない")]
public float wallTableGapX = 0.6f;
[Tooltip("卓の数を、いちばん狭い通路のときに入る数にそろえる。" +
"通路の幅の条件どうしを比べられるようにするため。外すと幅ごとに入るだけ並べる")]
public bool wallTableCountFromNarrowestAisle = true;
[Tooltip("席の前(人が立つ点)を、椅子・ベンチのふちからどれだけ離すか(m)。席に入れるかの確かめに使う")]
public float seatApproachGapZ = 0.3f;
[Header("席:島の卓")]
public int islandColumns = 3;
public int islandTablesPerColumn = 3;
[Tooltip("島の卓の大きさ(X, Z, m)")]
public Vector2 islandTableSize = new Vector2(0.75f, 1.2f);
[Tooltip("1列の中の卓どうしのすき間(Z, m)")]
public float islandTableGapZ = 0.1f;
[Header("椅子・卓の共通")]
[Tooltip("椅子の奥行き(通路側へ出る分, m)")]
public float chairDepth = 0.45f;
[Tooltip("椅子の幅(m)")]
public float chairWidth = 0.45f;
public float chairHeight = 0.45f;
public float tableHeight = 0.72f;
[Tooltip("背中合わせの席のあいだの仕切りの高さ(m)")]
public float dividerHeight = 1.1f;
[Header("入口まわり")]
[Tooltip("入口の幅(X, m)")]
public float entranceWidthX = 2.0f;
[Tooltip("風除室の奥行き(Z, m)")]
public float vestibuleDepthZ = 1.2f;
[Tooltip("待合の奥行き(Z, m)。0 なら下の壁ぞいの卓と同じ奥行きに合わせる")]
public float waitingDepthZ = 0f;
[Tooltip("レジの台の大きさ(X, Z, m)")]
public Vector2 registerSize = new Vector2(1.0f, 0.5f);
[Tooltip("レジの前に並ぶ点の数。会計に立つ人が1人ずつ並ぶ。埋まっていたら席で待つ")]
public int registerQueueCount = 3;
[Tooltip("レジの並ぶ点どうしの間隔(m)。レジの西へ順に並ぶ")]
public float registerQueuePitch = 0.5f;
[Tooltip("レジの内側(スタッフだけが入れる場所)の奥行き(Z, m)。" +
"幅はレジの台と同じ(registerSize.x)。レジ台の北に取り、" +
"太い通路(店の奥)からだけ入れる。客の並ぶ列や待合の中は通らない")]
public float registerBackDepthZ = 1.2f;
[Tooltip("レジの内側の入口(西側)に、歩ける幅としてこれだけは残す(m)。" +
"奥行きがこれ+通行半径×2 より小さいときは、自動で広げる")]
public float registerBackMinOpenZ = 0.7f;
[Tooltip("ドリンクバーの台の大きさ(X, Z, m)")]
public Vector2 drinkBarSize = new Vector2(3.6f, 0.7f);
[Tooltip("カウンター類の高さ(m)")]
public float counterHeight = 1.0f;
[Tooltip("レジ・ドリンクバー・トイレの前に人が立つ余地(m)")]
public float approachGap = 0.6f;
[Header("待合の広さ(B4)")]
[Tooltip("待合に立てる人数。この人数ぶんの広さを待合に確保する")]
public int waitingStandCount = 8;
[Tooltip("待合で1人が立つのに必要な広さ(m^2)")]
public float waitingStandAreaM2 = 0.5f;
[Header("ロボットの待機場所(B2)")]
[Tooltip("待機させる台数ぶんの点の数")]
public int robotStandbyCount = 2;
[Tooltip("太い通路の東のふち(パントリーのカウンター面)から、待機の点までの距離(m)")]
public float robotStandbySideOffset = 0.4f;
[Tooltip("受け渡し口から南へ、1台めの待機の点までの距離(m)。パントリーのカウンターぎわに下がる")]
public float robotStandbyFromPassZ = 1.6f;
[Tooltip("待機の点どうしの間隔(m)。ロボットの直径より大きくする")]
public float robotStandbyPitchZ = 0.8f;
[Tooltip("待機中の台の西に残す通り道の幅(m)。これを下回ると報告に出す")]
public float robotStandbyMinClearX = 0.55f;
[Header("スタッフの待つ場所")]
[Tooltip("手の空いたスタッフが待つ点の数(1人に1つ)")]
public int staffStandbyCount = 2;
[Tooltip("受け渡し口の窓口の北のふちから、待つ点までの距離(m)。" +
"ロボットは受け渡し口より北へ行かないので、ここは通り道をふさがない")]
public float staffStandbyFromPassZ = 0.45f;
[Tooltip("待つ点どうしの間隔(X, m)。人の直径より大きくする")]
public float staffStandbyPitchX = 0.6f;
[Tooltip("太い通路の東のふちから、1人めの待つ点までの距離(m)")]
public float staffStandbySideOffset = 0.4f;
[Header("ドリンクバー(B4)")]
[Tooltip("ドリンクバーの機械の数。前に立つ点をこの数だけ作る")]
public int drinkBarStandCount = 4;
[Tooltip("ドリンクバーの前に客が立つ場所の奥行き(m)")]
public float drinkBarStandDepthZ = 1.5f;
[Tooltip("ドリンクバーの端に並ぶ点の数")]
public int drinkBarQueueCount = 3;
[Tooltip("並ぶ点どうしの間隔(m)")]
public float drinkBarQueuePitch = 0.5f;
[Tooltip("並ぶ点の列の奥行き(m)。立つ場所の後ろに1列つくる")]
public float drinkBarQueueDepthZ = 0.6f;
[Tooltip("立つ場所・並ぶ点と、南の細い通路とのあいだに空ける分(m)")]
public float drinkBarAisleClearZ = 0.1f;
public override string StoreKindName { get { return "ファミレス型(客席+厨房+パントリー)"; } }
// ---- 帯の足し算で出る値 ----
/// <summary>島1列の幅(椅子+卓+椅子, m)</summary>
public float IslandColumnWidthX { get { return chairDepth * 2f + islandTableSize.x; } }
/// <summary>島1列の奥行き(卓 n 個ぶん, m)</summary>
public float IslandColumnDepthZ
{
get { return islandTablesPerColumn * islandTableSize.y + (islandTablesPerColumn - 1) * islandTableGapZ; }
}
/// <summary>ブース1つの奥行き(ベンチ+卓+ベンチ, m)</summary>
public float BoothUnitZ { get { return benchDepth * 2f + boothTableSize.y; } }
/// <summary>壁沿いの卓の列の奥行き(椅子+卓+ベンチ, m)</summary>
public float WallRowDepthZ { get { return chairDepth + wallTableSize.y + benchDepth; } }
/// <summary>南の帯(壁ぞいの卓の列)の奥行き(m)</summary>
float FrontDepthZ { get { return WallRowDepthZ; } }
float WaitingDepth { get { return waitingDepthZ > 0.0005f ? waitingDepthZ : FrontDepthZ; } }
// 西から東への境目
float BoothColumnEastX { get { return boothTableSize.x; } }
float Aisle1WestX { get { return BoothColumnEastX; } }
float IslandWestX { get { return Aisle1WestX + AisleWidth; } }
float IslandEastX { get { return IslandWestX + islandColumns * IslandColumnWidthX + (islandColumns - 1) * AisleWidth; } }
float MainAisleWestX { get { return IslandEastX + AisleWidth; } }
float MainAisleEastX { get { return MainAisleWestX + mainAisleWidth; } }
float ServiceWallX { get { return MainAisleEastX + serviceStripDepth; } }
float OuterEastX { get { return ServiceWallX + kitchenDepthX; } }
float MainAisleCenterX { get { return MainAisleWestX + mainAisleWidth * 0.5f; } }
// ---- 壁ぞいの2人席の並べ方 ----
//
// 上の列は島の帯いっぱい、下の列はドリンクバーの手前までを使う。
// 「卓 wallTableSize.x + すき間 wallTableGapX」の繰り返しで、入るだけ並べる。
// すき間は、残った幅を等分して広げる(いちばん狭いところは .asset の値)。
//
// 卓の数は、いちばん狭い通路のときに入る数にそろえる(wallTableCountFromNarrowestAisle)。
// 通路の幅を変えると壁の長さも変わるので、そろえないと条件ごとに卓の数が違ってしまい、
// 通路の幅の効果と卓の数の効果が混ざる。
/// <summary>細い通路の幅が w のときの、壁ぞいの列が使える幅(m)</summary>
float WallRowSpanXAt(float w)
{
return islandColumns * (IslandColumnWidthX + w);
}
/// <summary>上の列が使える幅(m)</summary>
float NorthRowSpanX { get { return WallRowSpanXAt(AisleWidth); } }
/// <summary>下の列が使える幅(m)。ドリンクバーのカウンターの分を引く</summary>
float SouthRowSpanX { get { return WallRowSpanXAt(AisleWidth) - drinkBarSize.x; } }
/// <summary>数をそろえるときに使う幅(いちばん狭い通路のときの幅, m)</summary>
float CountSpanX(bool north)
{
float w = wallTableCountFromNarrowestAisle ? MinAisleWidth : AisleWidth;
float span = WallRowSpanXAt(w);
return north ? span : span - drinkBarSize.x;
}
/// <summary>その幅に入る2人席の数</summary>
int WallTableCount(bool north)
{
float span = CountSpanX(north);
float unit = wallTableSize.x + wallTableGapX;
if (unit <= 0.0005f) return 0;
return Mathf.Max(0, Mathf.FloorToInt((span + wallTableGapX) / unit + 0.0001f));
}
public int NorthWallTableCount { get { return WallTableCount(true); } }
public int SouthWallTableCount { get { return WallTableCount(false); } }
/// <summary>
/// 席の前の点を、通路やすき間の「歩ける帯」の中に収める(m)。
/// 焼いた NavMesh は通行半径ぶん削られるので、ふちに寄せすぎると
/// 席に入れるのに「入れない」と出てしまう。
/// </summary>
float SeatApproachInset(float corridorWidth)
{
float min = navAgentRadius + navVoxelSize;
float max = Mathf.Max(min, corridorWidth - min);
return Mathf.Clamp(seatApproachGapZ, min, max);
}
/// <summary>その列の、卓と卓のあいだのすき間(m)。残った幅を等分して広げる</summary>
float WallTableGap(bool north)
{
int n = WallTableCount(north);
if (n <= 1) return wallTableGapX;
float span = north ? NorthRowSpanX : SouthRowSpanX;
float even = (span - n * wallTableSize.x) / (n - 1);
return Mathf.Max(wallTableGapX, even);
}
// 南から北への境目
float SouthRowNorthZ { get { return FrontDepthZ; } }
float SouthAisleNorthZ { get { return SouthRowNorthZ + AisleWidth; } }
float LowerIslandNorthZ { get { return SouthAisleNorthZ + IslandColumnDepthZ; } }
float CrossAisleNorthZ { get { return LowerIslandNorthZ + crossAisleWidth; } }
float UpperIslandNorthZ { get { return CrossAisleNorthZ + IslandColumnDepthZ; } }
float NorthAisleNorthZ { get { return UpperIslandNorthZ + AisleWidth; } }
/// <summary>帯の足し算だけで出る北のふち(m)。北の壁ぞいの卓は、ここを壁側のふちにする</summary>
float NorthRowOuterZ { get { return NorthAisleNorthZ + WallRowDepthZ; } }
/// <summary>ブースの列の長さ(m)</summary>
float BoothColumnNorthZ { get { return wallBoothCount * BoothUnitZ; } }
/// <summary>
/// 建物の北のふち(m)。帯の足し算と、ブースの列の長さの大きいほう。
/// ブースの列が長いときは、北の壁ぞいの卓の後ろ(壁とのあいだ)を壁でふさぐ。
/// </summary>
float OuterNorthZ { get { return Mathf.Max(NorthRowOuterZ, BoothColumnNorthZ); } }
/// <summary>店の外寸(幅 × 奥行き, m。風除室とアルコーブは含まない)</summary>
public Vector2 OuterSize { get { return new Vector2(OuterEastX, OuterNorthZ); } }
// ---- ドリンクバー(南の帯の東寄り。太い通路の西のふちに寄せる)----
float DrinkBarEastX { get { return MainAisleWestX; } }
float DrinkBarWestX { get { return DrinkBarEastX - drinkBarSize.x; } }
/// <summary>カウンター+立つ場所+並ぶ点+通路との隙間に必要な奥行き(m)</summary>
float DrinkBarNeedZ
{
get
{
return drinkBarSize.y + drinkBarStandDepthZ + drinkBarQueueDepthZ + drinkBarAisleClearZ;
}
}
/// <summary>南の帯に入りきらない分だけ、壁を南へ出す(くぼみの深さ, m)</summary>
float DrinkBarAlcoveZ { get { return Mathf.Max(0f, DrinkBarNeedZ - FrontDepthZ); } }
float DrinkBarSouthZ { get { return -DrinkBarAlcoveZ; } }
float DrinkBarNorthZ { get { return DrinkBarSouthZ + drinkBarSize.y; } }
float DrinkBarStandNorthZ { get { return DrinkBarNorthZ + drinkBarStandDepthZ; } }
float DrinkBarQueueNorthZ { get { return DrinkBarStandNorthZ + drinkBarQueueDepthZ; } }
// ---- 待合・レジ・入口(太い通路の東。トイレを北へ寄せて空けた帯)----
float FrontEastWestX { get { return MainAisleEastX; } }
/// <summary>レジの台とレジの内側の東のふち(=トイレの西のふち, m)</summary>
float RegisterEastX { get { return MainAisleEastX + registerSize.x; } }
/// <summary>待合・レジ・入口の帯の東のふち(m)</summary>
float FrontEastEastX { get { return RegisterEastX + toiletSize.x; } }
// レジの内側(スタッフだけが入れる場所)。
// レジ台の北、トイレの西。西(太い通路)からだけ入れる。
float RegisterBackSouthZ { get { return WaitingDepth; } }
/// <summary>
/// レジの内側の奥行き(m)。これが西の入口の幅になる。
/// 焼いた面で registerBackMinOpenZ を残せるよう、通行半径×2 を足した値を下回らせない。
/// </summary>
float RegisterBackDepth
{
get { return Mathf.Max(registerBackDepthZ, registerBackMinOpenZ + navAgentRadius * 2f); }
}
float RegisterBackNorthZ { get { return RegisterBackSouthZ + RegisterBackDepth; } }
/// <summary>レジの内側(スタッフだけが入れる場所)の広がり</summary>
PlanRect RegisterBackRect
{
get { return new PlanRect(MainAisleEastX, RegisterBackSouthZ, RegisterEastX, RegisterBackNorthZ); }
}
float ToiletSouthZ { get { return WaitingDepth; } }
float ToiletNorthZ { get { return ToiletSouthZ + toiletSize.y; } }
/// <summary>待合に必要な広さ(m^2)</summary>
float WaitingNeedAreaM2 { get { return Mathf.Max(0, waitingStandCount) * waitingStandAreaM2; } }
float IslandColumnWestX(int c) { return IslandWestX + c * (IslandColumnWidthX + AisleWidth); }
/// <summary>島の列と列のあいだ(および西端・東端)の細い通路の西のふち。c は 0..islandColumns</summary>
float ColumnAisleWestX(int c)
{
if (c <= 0) return Aisle1WestX;
return IslandColumnWestX(c - 1) + IslandColumnWidthX;
}
protected override StorePlan Compose()
{
StorePlan plan = new StorePlan();
float w = AisleWidth;
float h = wallHeight;
float t = wallThickness;
float off = DeliveryOffset();
float outerX = OuterEastX;
float outerZ = OuterNorthZ;
// 床は建物の中だけ。風除室とアルコーブはその分だけ別に敷く(建物の外に床が出ないように)
plan.AddFloor(new PlanRect(0f, 0f, outerX, outerZ));
// ---- 通路(0 番は太い通路)----
plan.AddAisle("MainAisle",
new PlanRect(MainAisleWestX, 0f, MainAisleEastX, outerZ), false,
new Vector3(MainAisleCenterX, 0f, OuterNorthZ * 0.5f));
int aisleSouthRow = plan.aisles.Count;
plan.AddAisle("SouthAisle",
new PlanRect(Aisle1WestX, SouthRowNorthZ, MainAisleWestX, SouthAisleNorthZ), true,
MainAisleWaitPoint((SouthRowNorthZ + SouthAisleNorthZ) * 0.5f));
// 横通路は太い通路あつかい(卓の aisleIndex には使わない)
plan.AddAisle("CrossAisle",
new PlanRect(Aisle1WestX, LowerIslandNorthZ, MainAisleWestX, CrossAisleNorthZ), false,
MainAisleWaitPoint((LowerIslandNorthZ + CrossAisleNorthZ) * 0.5f));
int aisleNorthRow = plan.aisles.Count;
plan.AddAisle("NorthAisle",
new PlanRect(Aisle1WestX, UpperIslandNorthZ, MainAisleWestX, NorthAisleNorthZ), true,
MainAisleWaitPoint((UpperIslandNorthZ + NorthAisleNorthZ) * 0.5f));
// 南北に通る細い通路。横通路で上下に分ける
int colAisleCount = islandColumns + 1;
int[] colLower = new int[colAisleCount];
int[] colUpper = new int[colAisleCount];
for (int c = 0; c < colAisleCount; c++)
{
float ax0 = ColumnAisleWestX(c);
float ax1 = ax0 + w;
string nm = c == 0 ? "WestAisle" : "ColumnAisle" + c.ToString("00");
// 西端の通路(ブースの前)は、南北いっぱいに通す
float lowZ0 = c == 0 ? 0f : SouthAisleNorthZ;
float lowZ1 = LowerIslandNorthZ;
float upZ0 = CrossAisleNorthZ;
float upZ1 = c == 0 ? outerZ : UpperIslandNorthZ;
colLower[c] = plan.aisles.Count;
plan.AddAisle(nm + "_S", new PlanRect(ax0, lowZ0, ax1, lowZ1), true,
MainAisleWaitPoint((lowZ0 + lowZ1) * 0.5f));
colUpper[c] = plan.aisles.Count;
plan.AddAisle(nm + "_N", new PlanRect(ax0, upZ0, ax1, upZ1), true,
MainAisleWaitPoint((upZ0 + upZ1) * 0.5f));
}
// ---- 外の壁 ----
ComposeOuterWalls(plan, h, t);
// ---- 厨房・事務所・トイレ・サービスの帯 ----
ComposeEastBlock(plan, h, t);
// ---- 席 ----
int tableId = 1;
ComposeBooths(plan, ref tableId, off, colLower, colUpper);
ComposeWallRow(plan, ref tableId, true, off, aisleNorthRow);
ComposeWallRow(plan, ref tableId, false, off, aisleSouthRow);
ComposeIslands(plan, ref tableId, off, colLower, colUpper);
// ---- 入口まわり(ドリンクバー・待合・レジ)----
ComposeFront(plan, h, t);
// ---- 余りをふさぐ ----
// 北西の角(ブースの列より北)
float boothEndZ = BoothColumnNorthZ;
if (outerZ - boothEndZ > 0.0005f)
plan.AddBox("NorthWestFiller", BoxStyle.Wall,
new PlanRect(0f, boothEndZ, BoothColumnEastX, outerZ), h);
// 北の壁ぞいの卓の後ろ(ブースの列のほうが長いときに出る余り)。
// 歩ける余地にすると北の通路が実質広がってしまうので、壁でふさぐ
if (outerZ - NorthRowOuterZ > 0.0005f)
plan.AddBox("NorthRowBackFiller", BoxStyle.Wall,
new PlanRect(IslandWestX, NorthRowOuterZ, MainAisleWestX, outerZ), h);
return plan;
}
/// <summary>太い通路の上で待つ点(横にずらして、戻る台が通れるようにする)</summary>
Vector3 MainAisleWaitPoint(float zoneCenterZ)
{
float z = Mathf.Clamp(zoneCenterZ - aisleEntranceSetback, 0.5f, OuterNorthZ - 0.5f);
return new Vector3(MainAisleCenterX - aisleEntranceSideOffset, 0f, z);
}
/// <summary>入口の中心(太い通路の東の帯のまん中)</summary>
float EntranceCenterX()
{
// レジの台・レジの内側より東(客が並ぶ側)のまん中
return (RegisterEastX + FrontEastEastX) * 0.5f;
}
// ---------------- 外の壁 ----------------
void ComposeOuterWalls(StorePlan plan, float h, float t)
{
float outerX = OuterEastX;
float outerZ = OuterNorthZ;
float entranceCenterX = EntranceCenterX();
float eL = entranceCenterX - entranceWidthX * 0.5f;
float eR = entranceCenterX + entranceWidthX * 0.5f;
// 南の壁。ドリンクバーのくぼみと入口のところを空ける(西から東へ)
float alcove = DrinkBarAlcoveZ;
int seg = 0;
if (alcove > 0.0005f)
{
AddSouthWall(plan, -t, DrinkBarWestX - t, h, t, ref seg);
AddSouthWall(plan, DrinkBarEastX + t, eL, h, t, ref seg);
}
else
{
AddSouthWall(plan, -t, eL, h, t, ref seg);
}
AddSouthWall(plan, eR, outerX + t, h, t, ref seg);
plan.AddBox("Wall_North", BoxStyle.Wall, new PlanRect(-t, outerZ, outerX + t, outerZ + t), h);
plan.AddBox("Wall_West", BoxStyle.Wall, new PlanRect(-t, -t, 0f, outerZ + t), h);
plan.AddBox("Wall_East", BoxStyle.Wall, new PlanRect(outerX, -t, outerX + t, outerZ + t), h);
// ドリンクバーのくぼみ(アルコーブ)。南の帯の奥行きが足りない分だけ外へ出す
if (alcove > 0.0005f)
{
float z0 = -alcove;
plan.AddFloor(new PlanRect(DrinkBarWestX - t, z0 - t, DrinkBarEastX + t, 0f));
plan.AddBox("DrinkBarAlcove_S", BoxStyle.Wall,
new PlanRect(DrinkBarWestX - t, z0 - t, DrinkBarEastX + t, z0), h);
plan.AddBox("DrinkBarAlcove_W", BoxStyle.Wall,
new PlanRect(DrinkBarWestX - t, z0, DrinkBarWestX, 0f), h);
plan.AddBox("DrinkBarAlcove_E", BoxStyle.Wall,
new PlanRect(DrinkBarEastX, z0, DrinkBarEastX + t, 0f), h);
}
// 風除室
if (vestibuleDepthZ > 0.0005f)
{
plan.AddFloor(new PlanRect(eL - t, -vestibuleDepthZ - t, eR + t, 0f));
plan.AddBox("Vestibule_W", BoxStyle.Wall, new PlanRect(eL - t, -vestibuleDepthZ - t, eL, 0f), h);
plan.AddBox("Vestibule_E", BoxStyle.Wall, new PlanRect(eR, -vestibuleDepthZ - t, eR + t, 0f), h);
plan.AddPlace(PlaceKind.Vestibule, "Vestibule",
new PlanRect(eL, -vestibuleDepthZ, eR, 0f), 0f,
new Vector3(entranceCenterX, 0f, -vestibuleDepthZ * 0.5f), false);
plan.AddPlace(PlaceKind.Entrance, "Entrance",
new PlanRect(eL, -0.1f, eR, 0.1f), 0f,
new Vector3(entranceCenterX, 0f, 0.5f), false);
}
}
void AddSouthWall(StorePlan plan, float x0, float x1, float h, float t, ref int seg)
{
if (x1 - x0 <= 0.0005f) return;
seg++;
plan.AddBox("Wall_South" + seg.ToString("00"), BoxStyle.Wall, new PlanRect(x0, -t, x1, 0f), h);
}
// ---------------- 東の塊(厨房・事務所・トイレ・サービス)----------------
void ComposeEastBlock(StorePlan plan, float h, float t)
{
float outerX = OuterEastX;
float outerZ = OuterNorthZ;
float mx = MainAisleEastX; // 太い通路の東のふち
float sx = ServiceWallX; // サービスの帯の東のふち(厨房の壁)
// トイレは、南の帯(待合・レジ・入口)を空けるために北へ寄せる
float tz0 = ToiletSouthZ;
float tz1 = ToiletNorthZ;
// レジの内側は太い通路(西)から入れるので、
// トイレの西の壁は tz0(=レジの内側の北のふち)から上だけに立てる。
// 厨房(通れない)
plan.AddPlace(PlaceKind.Kitchen, "Kitchen",
new PlanRect(sx, tz1, outerX, outerZ), h,
new Vector3((sx + outerX) * 0.5f, 0f, (tz1 + outerZ) * 0.5f), true);
// 事務所・更衣室(通れない)。南の帯まで下ろして、余りを作らない
float officeWestX = FrontEastEastX;
plan.AddPlace(PlaceKind.Office, "Office",
new PlanRect(officeWestX, 0f, outerX, tz1), h,
new Vector3((officeWestX + outerX) * 0.5f, 0f, tz1 * 0.5f), true);
// トイレ(西に扉。中は人が入れる)。
// 西のふちはレジの内側のぶん東へ寄せる(レジの内側は太い通路から入る)
float twx = RegisterEastX;
float doorC = (tz0 + tz1) * 0.5f;
float doorZ0 = Mathf.Max(tz0 + 0.3f, doorC - toiletDoorWidthZ * 0.5f);
float doorZ1 = Mathf.Min(tz1 - 0.3f, doorZ0 + toiletDoorWidthZ);
plan.AddBox("ToiletWall_W_S", BoxStyle.Wall, new PlanRect(twx, tz0, twx + t, doorZ0), h);
plan.AddBox("ToiletWall_W_N", BoxStyle.Wall, new PlanRect(twx, doorZ1, twx + t, tz1), h);
plan.AddBox("ToiletWall_S", BoxStyle.Wall, new PlanRect(twx, tz0, officeWestX, tz0 + t), h);
plan.AddBox("ToiletWall_N", BoxStyle.Wall, new PlanRect(twx, tz1 - t, officeWestX, tz1), h);
plan.AddBox("ToiletWall_E", BoxStyle.Wall, new PlanRect(officeWestX - t, tz0, officeWestX, tz1), h);
plan.AddPlace(PlaceKind.Toilet, "Toilet",
new PlanRect(twx + t, tz0 + t, officeWestX - t, tz1 - t), 0f,
new Vector3((twx + officeWestX) * 0.5f, 0f, doorC), false);
// サービスの帯(受け渡し口・パントリー・返却口・残りは壁)
float passC = outerZ - passFromNorthZ;
float passZ0 = passC - passWindowLengthZ * 0.5f;
float passZ1 = passC + passWindowLengthZ * 0.5f;
float pantryZ1 = outerZ - pantryFromNorthZ;
float pantryZ0 = pantryZ1 - pantryLengthZ;
float retC = outerZ - returnFromNorthZ;
float retZ0 = retC - returnWindowLengthZ * 0.5f;
float retZ1 = retC + returnWindowLengthZ * 0.5f;
plan.AddPlace(PlaceKind.FoodPass, "FoodPass",
new PlanRect(mx, passZ0, sx, passZ1), counterHeight,
new Vector3(MainAisleCenterX, 0f, passC), true);
plan.AddPlace(PlaceKind.Pantry, "Pantry",
new PlanRect(mx, pantryZ0, sx, pantryZ1), counterHeight,
new Vector3(MainAisleCenterX, 0f, (pantryZ0 + pantryZ1) * 0.5f), true);
// ロボットの待機場所(B2)。受け渡し口の前の点とは別にする。
// ・受け渡し口の南側(パントリーのカウンターぎわ)に下がる。
// 受け渡し口の前にはスタッフも料理を取りに来るので、ロボットが居座らないようにする
// ・太い通路の東のふち(カウンター面)に寄せる。西のふちは細い通路の出入口なので使わない
// ・受け渡し口・パントリー・返却口の前の点は、どれも太い通路の真ん中
// (スタッフが立つ点)なので、そこから外れた東のふちに置く
// ・台どうしは robotStandbyPitchZ だけ南北に離す(重ならないように)
// ・西側には通り道を残す(残りの幅は store_report に出す)
float standbyX = MainAisleEastX - robotStandbySideOffset;
float standbyZ0 = passC - robotStandbyFromPassZ;
for (int i = 0; i < Mathf.Max(0, robotStandbyCount); i++)
{
float z = Mathf.Max(0.5f, standbyZ0 - i * robotStandbyPitchZ);
plan.AddPlace(PlaceKind.RobotStandby, "RobotStandby" + (i + 1).ToString("00"),
new PlanRect(0f, 0f, 0f, 0f), 0f, new Vector3(standbyX, 0f, z), false);
}
// 手の空いたスタッフが待つ点。
// 受け渡し口の窓口より北は太い通路の行き止まり(ロボットの行き先は
// 受け渡し口・パントリー・返却口・待機場所で、すべてこれより南)なので、
// ここに2人ぶん並べればロボットの通り道をふさがない。
// パントリーのカウンターの先は厨房で通れないため、カウンターの内側には立てない。
for (int i = 0; i < Mathf.Max(0, staffStandbyCount); i++)
{
float x = mx - staffStandbySideOffset - i * staffStandbyPitchX;
if (x < MainAisleWestX + 0.2f) break;
float z = Mathf.Min(passZ1 + staffStandbyFromPassZ, outerZ - 0.45f);
plan.AddPlace(PlaceKind.StaffStandby, "StaffStandby" + (i + 1).ToString("00"),
new PlanRect(0f, 0f, 0f, 0f), 0f, new Vector3(x, 0f, z), false);
}
plan.AddPlace(PlaceKind.DishReturn, "DishReturn",
new PlanRect(mx, retZ0, sx, retZ1), counterHeight,
new Vector3(MainAisleCenterX, 0f, retC), true);
// 帯の残りを壁でふさぐ(南から北へ、窓口とカウンターのあいだ)
float[] marks = { tz1, retZ0, retZ1, pantryZ0, pantryZ1, passZ0, passZ1, outerZ };
for (int i = 0; i < marks.Length - 1; i += 2)
{
float z0 = marks[i], z1 = marks[i + 1];
if (z1 - z0 > 0.0005f)
plan.AddBox("ServiceWall" + i, BoxStyle.Wall, new PlanRect(mx, z0, sx, z1), h);
}
}
// ---------------- 席 ----------------
void ComposeBooths(StorePlan plan, ref int tableId, float off, int[] colLower, int[] colUpper)
{
float x0 = 0f, x1 = BoothColumnEastX;
float unitZ = BoothUnitZ;
float aisleCenterX = Aisle1WestX + AisleWidth * 0.5f;
for (int i = 0; i < wallBoothCount; i++)
{
float z0 = i * unitZ;
float tableZ0 = z0 + benchDepth;
float tableZ1 = tableZ0 + boothTableSize.y;
// 卓の手前と奥のベンチ(1本に2人ぶん)。背もたれは卓から遠い側に立てる
AddBenchZ(plan, "BoothBench_S", x0, x1, z0, tableZ0, 2);
AddBenchZ(plan, "BoothBench_N", x0, x1, z0 + unitZ, tableZ1, 2);
PlanSeat seat = new PlanSeat();
seat.tableId = tableId++;
seat.kind = SeatKind.WallBooth;
seat.seatCount = 4;
seat.tableRect = new PlanRect(x0, tableZ0, x1, tableZ1);
seat.tableHeight = tableHeight;
float cz = (tableZ0 + tableZ1) * 0.5f;
seat.deliveryPoint = new Vector3(aisleCenterX - off, 0f, cz);
seat.aisleIndex = cz < LowerIslandNorthZ ? colLower[0] : colUpper[0];
// 席の前の点。2人ぶんのベンチには、通路側から同じ口で入る
float spotX = Aisle1WestX + SeatApproachInset(AisleWidth);
seat.spots.Add(new PlanSeatSpot("南ベンチ(2人)",
new Vector3(spotX, 0f, (z0 + tableZ0) * 0.5f)));
seat.spots.Add(new PlanSeatSpot("北ベンチ(2人)",
new Vector3(spotX, 0f, (tableZ1 + z0 + unitZ) * 0.5f)));
plan.seats.Add(seat); // ブースは椅子を引かない(ベンチ)
}
}
/// <summary>
/// 壁ぞいの2人席の列。壁側にベンチ1人、通路側に椅子1人。
/// 卓と卓のあいだは仕切らず、すき間を空ける(ここを通って壁側の席に入る)。
/// </summary>
void ComposeWallRow(StorePlan plan, ref int tableId, bool north, float off, int aisleIndex)
{
int count = WallTableCount(north);
if (count <= 0) return;
float tableW = wallTableSize.x;
float gap = WallTableGap(north);
float rowWestX = IslandWestX;
// 北の列:壁側(北)にベンチ、通路側(南)に椅子。南の列はその逆。
// 北の列の壁側のふちは、帯の足し算で出る位置(建物の北のふちではない)
float rowOuterZ = north ? NorthRowOuterZ : 0f;
float dir = north ? -1f : 1f; // 壁から通路へ向かう向き
float benchZ0 = rowOuterZ;
float benchZ1 = rowOuterZ + dir * benchDepth;
float tableZ0 = benchZ1;
float tableZ1 = tableZ0 + dir * wallTableSize.y;
float chairZ0 = tableZ1;
float chairZ1 = chairZ0 + dir * chairDepth;
// 壁側の席の前に立つ点の z。ベンチのまん中だが、壁に寄りすぎると
// 焼いた NavMesh から外れるので、壁から通行半径ぶんは離す
float benchStandZ = benchZ0 + dir * Mathf.Max(benchDepth * 0.5f, navAgentRadius + navVoxelSize);
float aisleCenterZ = north
? (UpperIslandNorthZ + NorthAisleNorthZ) * 0.5f
: (SouthRowNorthZ + SouthAisleNorthZ) * 0.5f;
float deliveryZ = aisleCenterZ + (north ? off : -off);
for (int i = 0; i < count; i++)
{
float tx0 = rowWestX + i * (tableW + gap);
float tx1 = tx0 + tableW;
float cx = (tx0 + tx1) * 0.5f;
// 壁側のベンチ(1人ぶん)
AddBenchZ(plan, "WallBench", tx0, tx1, benchZ0, benchZ1, 1);
PlanSeat seat = new PlanSeat();
seat.tableId = tableId++;
seat.kind = SeatKind.WallTable;
seat.seatCount = 2;
seat.tableRect = new PlanRect(tx0, tableZ0, tx1, tableZ1);
seat.tableHeight = tableHeight;
seat.deliveryPoint = new Vector3(cx, 0f, deliveryZ);
seat.aisleIndex = aisleIndex;
// 通路側の椅子1脚(引ける)
PlanChair chair = new PlanChair();
chair.rect = new PlanRect(cx - chairWidth * 0.5f, Mathf.Min(chairZ0, chairZ1),
cx + chairWidth * 0.5f, Mathf.Max(chairZ0, chairZ1));
chair.height = chairHeight;
chair.pullable = true;
chair.pullDirection = new Vector3(0f, 0f, dir);
seat.chairs.Add(chair);
// 席の前の点。
// 壁側:卓のとなりのすき間を通って入る。最後の卓は西側のすき間を使う
// 通路側:椅子の通路側のふちから少し離れた点(通路の上)
float gapCenterX = i < count - 1 ? tx1 + gap * 0.5f : tx0 - gap * 0.5f;
seat.spots.Add(new PlanSeatSpot("壁側", new Vector3(gapCenterX, 0f, benchStandZ)));
// 卓と卓のすき間は、人がロボットをよけて下がれる場所にもなる。
// 通路の外(卓の奥行きのまん中あたり)に取る
if (i < count - 1)
plan.refuges.Add(new Vector3(tx1 + gap * 0.5f, 0f,
rowOuterZ + dir * (benchDepth + wallTableSize.y * 0.5f)));
seat.spots.Add(new PlanSeatSpot("通路側",
new Vector3(cx, 0f, chairZ1 + dir * SeatApproachInset(AisleWidth))));
plan.seats.Add(seat);
}
}
/// <summary>
/// ベンチ1つを、座面と背もたれに分けて置く。奥行きは
/// 「背もたれ benchBackThickness + 座面(残り)」で、合わせて benchDepth。
/// zOuter は卓から遠い側(壁側・背中合わせの側)、zInner は卓に近い側。
/// 背もたれは zOuter 側に立てるので、背中合わせの席では仕切りを兼ねる。
///
/// 座面の上に「座る場所」の見た目を2つ敷く(1本=2人ぶん)。
/// これは当たりを持たない板なので、NavMesh・前のセンサー・左右の空きの測りには
/// いっさい効かない(見た目だけ)。座面の箱そのものは1つのまま変えない。
/// </summary>
void AddBenchZ(StorePlan plan, string name, float x0, float x1, float zOuter, float zInner, int seats)
{
float dir = zInner >= zOuter ? 1f : -1f;
float back = zOuter + dir * Mathf.Min(benchBackThickness, Mathf.Abs(zInner - zOuter));
plan.AddBox(name + "_Back", BoxStyle.Bench,
new PlanRect(x0, Mathf.Min(zOuter, back), x1, Mathf.Max(zOuter, back)), benchHeight);
float seatZ0 = Mathf.Min(back, zInner);
float seatZ1 = Mathf.Max(back, zInner);
plan.AddBox(name + "_Seat", BoxStyle.Bench,
new PlanRect(x0, seatZ0, x1, seatZ1), benchSeatHeight);
AddBenchSeatLook(plan, name, x0, x1, seatZ0, seatZ1, seats);
}
/// <summary>
/// 座る場所の見た目を、座面の上に seats 個ぶん敷く(当たりなし)。
/// 1本の長さを seats で割り、となりと接する側だけ benchSeatGapX ずつ詰めて四角にする
/// (2人ぶんならまん中に benchSeatGapX × 2 のすき間が開く。1人ぶんなら詰めない)。
/// </summary>
void AddBenchSeatLook(StorePlan plan, string name, float x0, float x1, float z0, float z1, int seats)
{
if (benchSeatPadThickness <= 0.0005f) return;
if (seats <= 0) return;
float cell = (x1 - x0) / seats;
for (int i = 0; i < seats; i++)
{
float px0 = x0 + i * cell + (i > 0 ? benchSeatGapX : 0f);
float px1 = x0 + (i + 1) * cell - (i < seats - 1 ? benchSeatGapX : 0f);
if (px1 - px0 <= 0.05f) continue; // 短すぎて分けられない
PlanBox b = plan.AddBox(name + "_SeatLook" + (i + 1), BoxStyle.BenchSeat,
new PlanRect(px0, z0, px1, z1), benchSeatPadThickness);
if (b != null) b.baseY = benchSeatHeight; // 座面の上に乗せる
}
}
void ComposeIslands(StorePlan plan, ref int tableId, float off, int[] colLower, int[] colUpper)
{
float w = AisleWidth;
for (int half = 0; half < 2; half++)
{
bool upper = half == 1;
float z0 = upper ? CrossAisleNorthZ : SouthAisleNorthZ;
for (int c = 0; c < islandColumns; c++)
{
float colX0 = IslandColumnWestX(c);
float tableX0 = colX0 + chairDepth;
float tableX1 = tableX0 + islandTableSize.x;
float colX1 = tableX1 + chairDepth;
// 料理は東側の通路から出す(西の通路に偏らないように)
int eastAisle = upper ? colUpper[c + 1] : colLower[c + 1];
float eastAisleCenterX = ColumnAisleWestX(c + 1) + w * 0.5f;
for (int r = 0; r < islandTablesPerColumn; r++)
{
float tz0 = z0 + r * (islandTableSize.y + islandTableGapZ);
float tz1 = tz0 + islandTableSize.y;
float cz = (tz0 + tz1) * 0.5f;
PlanSeat seat = new PlanSeat();
seat.tableId = tableId++;
seat.kind = SeatKind.IslandTable;
seat.seatCount = 4;
seat.tableRect = new PlanRect(tableX0, tz0, tableX1, tz1);
seat.tableHeight = tableHeight;
seat.deliveryPoint = new Vector3(eastAisleCenterX - off, 0f, cz);
seat.aisleIndex = eastAisle;
// 両側に椅子2脚ずつ(引ける)
for (int side = -1; side <= 1; side += 2)
{
float cx0 = side < 0 ? colX0 : tableX1;
float cx1 = side < 0 ? tableX0 : colX1;
for (int k = -1; k <= 1; k += 2)
{
float ccz = cz + k * (islandTableSize.y * 0.25f);
PlanChair chair = new PlanChair();
chair.rect = new PlanRect(cx0, ccz - chairWidth * 0.5f, cx1, ccz + chairWidth * 0.5f);
chair.height = chairHeight;
chair.pullable = true;
chair.pullDirection = new Vector3(side, 0f, 0f);
seat.chairs.Add(chair);
// 席の前の点(椅子の通路側のふちから少し離れた、通路の上)
float inset = SeatApproachInset(w);
float spotX = side < 0 ? colX0 - inset : colX1 + inset;
seat.spots.Add(new PlanSeatSpot(
(side < 0 ? "西" : "東") + (k < 0 ? "南" : "北"),
new Vector3(spotX, 0f, ccz)));
}
}
plan.seats.Add(seat);
// 1列の中の卓どうしのすき間をふさぐ
if (r < islandTablesPerColumn - 1 && islandTableGapZ > 0.0005f)
plan.AddBox("IslandGap", BoxStyle.Divider,
new PlanRect(colX0, tz1, colX1, tz1 + islandTableGapZ), dividerHeight);
}
}
}
}
// ---------------- 入口まわり ----------------
void ComposeFront(StorePlan plan, float h, float t)
{
// ---- ドリンクバー(B4)----
// カウンターは太い通路の西のふちに寄せる。壁卓3つとのあいだは通り道。
// 奥行きは「カウンター+立つ場所+並ぶ点+通路との隙間」で決まり、
// 南の帯に入りきらない分だけ、ここだけ壁を南へ出してくぼみにする。
float dbX0 = DrinkBarWestX;
float dbX1 = DrinkBarEastX;
float barNorth = DrinkBarNorthZ;
float standNorth = DrinkBarStandNorthZ;
float queueNorth = DrinkBarQueueNorthZ;
float standZ = (barNorth + standNorth) * 0.5f;
float queueZ = (standNorth + queueNorth) * 0.5f;
plan.AddPlace(PlaceKind.DrinkBar, "DrinkBar",
new PlanRect(dbX0, DrinkBarSouthZ, dbX1, barNorth), counterHeight,
new Vector3((dbX0 + dbX1) * 0.5f, 0f, standZ), true);
// 前に立つ点(機械1台に1つ)。カウンターの長さを等分する
int stands = Mathf.Max(0, drinkBarStandCount);
for (int i = 0; i < stands; i++)
{
float x = dbX0 + drinkBarSize.x * (i + 0.5f) / stands;
plan.AddPlace(PlaceKind.DrinkBarStand, "DrinkBarStand" + (i + 1).ToString("00"),
new PlanRect(0f, 0f, 0f, 0f), 0f, new Vector3(x, 0f, standZ), false);
}
// 並ぶ点は、立つ場所の後ろに1列つくる。カウンターの東の端から西へ並ぶ。
// 南の細い通路(SouthAisle)には入らない奥行きに収める
int queues = Mathf.Max(0, drinkBarQueueCount);
for (int i = 0; i < queues; i++)
{
float x = dbX1 - drinkBarQueuePitch * (i + 0.5f);
if (x < dbX0 + 0.05f) break;
plan.AddPlace(PlaceKind.DrinkBarQueue, "DrinkBarQueue" + (i + 1).ToString("00"),
new PlanRect(0f, 0f, 0f, 0f), 0f, new Vector3(x, 0f, queueZ), false);
}
// ---- レジ・レジの内側・待合・入口(太い通路の東の帯)----
//
// レジ台をはさんで、南が客側・北が店員側。
// 店員側(レジの内側):幅 registerSize.x × 奥行き registerBackDepthZ。
// 西(太い通路=店の奥)からだけ入る。客の並ぶ列や待合の中は通らない
// 客側:レジ台の南に並ぶ点。店員側には入れない
// (人用の NavMesh でも、PeopleSpec.registerBackArea の区画にして客を入れない)
float feX0 = FrontEastWestX;
float feX1 = FrontEastEastX;
float feZ = WaitingDepth;
// レジの台(客側と店員側のさかい)
float regX0 = feX0;
float regX1 = RegisterEastX;
float regZ1 = feZ;
float regZ0 = regZ1 - registerSize.y;
plan.AddPlace(PlaceKind.Register, "Register",
new PlanRect(regX0, regZ0, regX1, regZ1), counterHeight,
new Vector3((regX0 + regX1) * 0.5f, 0f, regZ0 - approachGap), true);
// 店員側(レジの内側)。北を壁でふさぎ、東はトイレの西の壁が兼ねる。
// 南はレジの台、西だけが開いている(太い通路から入る)
PlanRect back = RegisterBackRect;
if (!back.IsEmpty)
{
plan.AddBox("RegisterBackWall_N", BoxStyle.Wall,
new PlanRect(back.xMin, back.zMax, back.xMax, back.zMax + t), h);
plan.AddPlace(PlaceKind.RegisterBack, "RegisterBack",
back, 0f, new Vector3(back.CenterX, 0f, back.CenterZ), false);
}
// 客が並ぶ点。レジの台の南、東へ順に並べる
float regQueueZ = regZ0 - approachGap;
for (int i = 0; i < Mathf.Max(0, registerQueueCount); i++)
{
float x = regX0 + 0.4f + i * registerQueuePitch;
if (x > feX1 - 0.25f) break;
plan.AddPlace(PlaceKind.RegisterQueue, "RegisterQueue" + (i + 1).ToString("00"),
new PlanRect(0f, 0f, 0f, 0f), 0f, new Vector3(x, 0f, regQueueZ), false);
}
// 待合(レジの台より東。客側)
plan.AddPlace(PlaceKind.Waiting, "Waiting",
new PlanRect(regX1, 0f, feX1, feZ), 0f,
new Vector3((regX1 + feX1) * 0.5f, 0f, feZ * 0.5f), false);
}
// ---------------- 報告(Build Store が出す)----------------
public override string FrontFitReport()
{
int nNorth = NorthWallTableCount;
int nSouth = SouthWallTableCount;
float gapNorth = WallTableGap(true);
float gapSouth = WallTableGap(false);
float southRowEast = IslandWestX + nSouth * wallTableSize.x + (nSouth > 1 ? (nSouth - 1) * gapSouth : 0f);
float barRoom = MainAisleWestX - southRowEast;
float personGap = navAgentRadius * 2f;
float waitGross = (FrontEastEastX - RegisterEastX) * WaitingDepth;
float waitNet = waitGross; // レジの台と店員側は待合の外に出したので、引かない
string s = "";
s += "外寸(建物だけ): " + N(OuterEastX) + " × " + N(OuterNorthZ) + " m\n";
s += " 南の帯の奥行き: " + N(FrontDepthZ) + " m(椅子 " + N(chairDepth) +
" + 卓 " + N(wallTableSize.y) + " + ベンチ " + N(benchDepth) + ")\n";
s += " 北のふち: 帯の足し算 " + N(NorthRowOuterZ) + " m / ブースの列 " +
N(BoothColumnNorthZ) + " m(ブース " + wallBoothCount + " × " + N(BoothUnitZ) + " m)→ 長いほうを採る";
if (OuterNorthZ - NorthRowOuterZ > 0.0005f)
s += "\n 北の壁ぞいの卓の後ろ " + N(OuterNorthZ - NorthRowOuterZ) + " m は壁でふさいだ(NorthRowBackFiller)";
s += "\n";
s += "壁ぞいの2人席: 卓 " + N(wallTableSize.x) + " × " + N(wallTableSize.y) + " m\n";
s += " 上の列: " + nNorth + " つ / 使える幅 " + N(NorthRowSpanX) +
" m / すき間 " + N(gapNorth) + " m\n";
s += " 下の列: " + nSouth + " つ / 使える幅 " + N(SouthRowSpanX) +
" m(ドリンクバーの " + N(drinkBarSize.x) + " m を引いた残り)/ すき間 " + N(gapSouth) + " m\n";
s += " 卓の数の決め方: " +
(wallTableCountFromNarrowestAisle
? "いちばん狭い通路 " + N(MinAisleWidth) + " m のときに入る数にそろえた(条件どうしを比べるため)"
: "いまの通路の幅で入るだけ並べた(幅ごとに数が変わる)") + "\n";
s += " すき間と人の通れる幅: いちばん狭いすき間 " + N(Mathf.Min(gapNorth, gapSouth)) +
" m / 焼いた NavMesh の通行幅 " + N(personGap) + " m(navAgentRadius × 2)→ " +
(Mathf.Min(gapNorth, gapSouth) >= personGap + 0.0005f
? "壁側の席に入れる" : "入れない ← wallTableGapX を広げる") + "\n";
s += "ドリンクバー: カウンター " + N(drinkBarSize.x) + " × " + N(drinkBarSize.y) +
" m を x " + N(DrinkBarWestX) + ".." + N(DrinkBarEastX) + " に置いた(下の列の東の残り " +
N(barRoom) + " m)" + (barRoom + 0.0005f >= drinkBarSize.x ? "" : " ← 入りきらない") + "\n";
s += " 奥行き: カウンター " + N(drinkBarSize.y) + " + 立つ場所 " + N(drinkBarStandDepthZ) +
" + 並ぶ点の列 " + N(drinkBarQueueDepthZ) + " + 通路との隙間 " + N(drinkBarAisleClearZ) +
" = " + N(DrinkBarNeedZ) + " m\n";
s += " くぼみ(アルコーブ): 南へ " + N(DrinkBarAlcoveZ) + " m(" + N(drinkBarSize.x) +
" m 幅ぶんだけ壁を外へ出した)\n";
s += " 立つ点 " + drinkBarStandCount + " つ(z " + N(standZOf()) + ")/ 並ぶ点 " +
drinkBarQueueCount + " つ(z " + N(queueZOf()) + ")\n";
s += " 並ぶ点の北のふち " + N(DrinkBarQueueNorthZ) + " m < 南の細い通路の南のふち " +
N(SouthRowNorthZ) + " m → " +
(DrinkBarQueueNorthZ <= SouthRowNorthZ + 0.0005f ? "通路と重ならない" : "通路に重なる ← 直す必要あり") + "\n";
s += "待合: x " + N(RegisterEastX) + ".." + N(FrontEastEastX) + " / 奥行き " + N(WaitingDepth) +
" m → " + N(waitGross) + " m^2(レジの台と店員側は含まない)\n";
s += " 必要な広さ: " + waitingStandCount + " 人 × " + N(waitingStandAreaM2) + " m^2 = " +
N(WaitingNeedAreaM2) + " m^2 → " +
(waitNet + 0.0005f >= WaitingNeedAreaM2 ? "足りている" : "足りない ← 直す必要あり") + "\n";
PlanRect back = RegisterBackRect;
s += "レジ: 台 " + N(registerSize.x) + " × " + N(registerSize.y) +
" m(x " + N(MainAisleEastX) + ".." + N(RegisterEastX) + ")。" +
"台をはさんで南が客側・北が店員側\n";
s += " 店員側(レジの内側): " + N(back.SizeX) + " × " + N(back.SizeZ) +
" m(" + back.ToString() + ")/ スタッフの立つ点 (" +
N(back.CenterX) + ", " + N(back.CenterZ) + ")\n";
s += " 入口は西(太い通路 x " + N(MainAisleEastX) + ")だけ、幅 " + N(RegisterBackDepth) +
" m。北は壁、東はトイレの西の壁、南はレジの台 → 客の列や待合の中は通らない\n";
s += " 焼いた面で残る入口の幅の見込み: 人用 " +
N(RegisterBackDepth - 0.4f) + " m / ロボット用 " +
N(RegisterBackDepth - navAgentRadius * 2f) + " m(必要 " +
N(registerBackMinOpenZ) + " m 以上)\n";
s += " 客の並ぶ点 " + registerQueueCount + " つ(レジ台の南, z " +
N(WaitingDepth - registerSize.y - approachGap) + ")。" +
"店員側は NavMesh の区画を分けてあるので、客は入れない\n";
s += "入口: 中心 x " + N(EntranceCenterX()) + " / 幅 " + N(entranceWidthX) +
" m(太い通路の東。トイレは z " + N(ToiletSouthZ) + ".." + N(ToiletNorthZ) + " へ北寄せ)";
return s;
}
public override string RobotStandbyReport()
{
StorePlan plan = NewPlan();
if (plan == null) return "";
System.Collections.Generic.List<Vector3> pts = plan.PointsOf(PlaceKind.RobotStandby);
if (pts.Count == 0) return "待機の点がありません(robotStandbyCount = " + robotStandbyCount + ")";
float r = navAgentRadius; // ロボットの半径に合わせた値
Vector3 pass = plan.PointOf(PlaceKind.FoodPass, Vector3.zero);
Vector3 pantry = plan.PointOf(PlaceKind.Pantry, Vector3.zero);
Vector3 ret = plan.PointOf(PlaceKind.DishReturn, Vector3.zero);
string s = "太い通路: x " + N(MainAisleWestX) + ".." + N(MainAisleEastX) +
"(幅 " + N(mainAisleWidth) + " m)/ ロボットの半径 " + N(r) + " m\n";
float minPitch = float.MaxValue;
float minService = float.MaxValue;
float minClear = float.MaxValue;
for (int i = 0; i < pts.Count; i++)
{
Vector3 p = pts[i];
float westClear = (p.x - r) - MainAisleWestX; // 西側に残る通り道
float eastGap = MainAisleEastX - (p.x + r); // 東のカウンター面までの空き
float dPass = Dist2D(p, pass);
float dPantry = Dist2D(p, pantry);
float dRet = Dist2D(p, ret);
if (westClear < minClear) minClear = westClear;
float svc = Mathf.Min(dPass, Mathf.Min(dPantry, dRet));
if (svc < minService) minService = svc;
if (i > 0)
{
float d = Dist2D(p, pts[i - 1]);
if (d < minPitch) minPitch = d;
}
s += " 待機" + (i + 1).ToString("00") + ": (" + N(p.x) + ", " + N(p.z) + ")" +
" / 西に残る通り道 " + N(westClear) + " m / 東のカウンター面まで " + N(eastGap) + " m" +
" / 受け渡し口の前の点まで " + N(dPass) + " m・パントリー " + N(dPantry) +
" m・返却口 " + N(dRet) + " m\n";
}
if (pts.Count >= 2)
s += " 点どうしのいちばん近い距離: " + N(minPitch) + " m → " +
(minPitch >= r * 2f + 0.0005f ? "重ならない(直径 " + N(r * 2f) + " m)"
: "重なる ← 直す必要あり") + "\n";
else
s += " 点は1つだけなので、重なりの確かめは要らない\n";
s += " 西に残る通り道のいちばん狭いところ: " + N(minClear) + " m → " +
(minClear >= robotStandbyMinClearX - 0.0005f
? "太い通路をふさがない(必要 " + N(robotStandbyMinClearX) + " m 以上)"
: "ふさぐ ← 直す必要あり(必要 " + N(robotStandbyMinClearX) + " m 以上)") + "\n";
s += " スタッフが立つ点(受け渡し口・パントリー・返却口の前。どれも通路の真ん中)までの" +
"いちばん近い距離: " + N(minService) + " m → " +
(minService >= r * 2f + 0.0005f ? "重ならない" : "重なる ← 直す必要あり") + "\n";
// 手の空いたスタッフが待つ点
System.Collections.Generic.List<Vector3> st = plan.PointsOf(PlaceKind.StaffStandby);
float pr = 0.2f; // 人の半径(PeopleSpec の仮の値に合わせる)
s += "スタッフの待つ点: " + st.Count + " つ";
if (st.Count == 0) return s + "(無し ← staffStandbyCount を見る)";
float stMinPitch = float.MaxValue;
float stMinClear = float.MaxValue;
for (int i = 0; i < st.Count; i++)
{
float west = (st[i].x - pr) - MainAisleWestX;
float east = MainAisleEastX - (st[i].x + pr);
if (Mathf.Min(west, east) < stMinClear) stMinClear = Mathf.Min(west, east);
if (i > 0) stMinPitch = Mathf.Min(stMinPitch, Dist2D(st[i], st[i - 1]));
s += "\n 待つ" + (i + 1).ToString("00") + ": (" + N(st[i].x) + ", " + N(st[i].z) + ")" +
" / 太い通路の西のふちまで " + N(west) + " m・東のふちまで " + N(east) + " m";
}
s += "\n 受け渡し口の窓口の北のふち z " + N(OuterNorthZ - passFromNorthZ + passWindowLengthZ * 0.5f) +
" より北 → ロボットの行き先(受け渡し口 " +
N(OuterNorthZ - passFromNorthZ) + " ・パントリー・返却口・待機場所)はすべて南なので、" +
"通り道をふさがない";
if (st.Count >= 2)
s += "\n 点どうしのいちばん近い距離: " + N(stMinPitch) + " m → " +
(stMinPitch >= pr * 2f + 0.0005f ? "重ならない(人の直径 " + N(pr * 2f) + " m)"
: "重なる ← 直す必要あり");
s += "\n 待っている人の左右に残る幅のいちばん狭いところ: " + N(stMinClear) + " m" +
"(太い通路 " + N(mainAisleWidth) + " m のうち)";
return s;
}
static float Dist2D(Vector3 a, Vector3 b)
{
float dx = a.x - b.x, dz = a.z - b.z;
return Mathf.Sqrt(dx * dx + dz * dz);
}
float standZOf() { return (DrinkBarNorthZ + DrinkBarStandNorthZ) * 0.5f; }
float queueZOf() { return (DrinkBarStandNorthZ + DrinkBarQueueNorthZ) * 0.5f; }
static string N(float v)
{
return v.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture);
}
}
}
Store/Chair.cs
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// 椅子1脚。通路側の椅子だけが引かれる。
/// 引かれているあいだは NavMeshObstacle(carve あり)になって通路をふさぐ。
/// 引く・戻すの判断は People 側(SeatedGuest)が持つ。この script は椅子の状態だけを持つ。
/// </summary>
public class Chair : MonoBehaviour
{
[Tooltip("通路側の椅子か。通路側の椅子だけが引かれる")]
public bool isAisleSide;
[Tooltip("引くときに動く向き(通路の側へ)。XZ 平面の単位ベクトル")]
public Vector3 pullDirection = Vector3.forward;
[Tooltip("しまってあるときの位置")]
public Vector3 parkedLocalPosition;
NavMeshObstacle _obstacle;
bool _pulled;
public bool IsPulled { get { return _pulled; } }
void Awake()
{
_obstacle = GetComponent<NavMeshObstacle>();
if (_obstacle != null) _obstacle.enabled = false;
}
/// <summary>StoreBuilder が置いたあとに呼ぶ。いまの位置をしまってある位置として覚える</summary>
public void MarkParked()
{
parkedLocalPosition = transform.localPosition;
}
/// <summary>通路側へ distance だけ引く</summary>
public void Pull(float distance)
{
if (_pulled || !isAisleSide) return;
_pulled = true;
transform.localPosition = parkedLocalPosition + pullDirection.normalized * distance;
if (_obstacle != null) _obstacle.enabled = true;
}
/// <summary>もとの位置に戻す</summary>
public void Release()
{
if (!_pulled) return;
_pulled = false;
transform.localPosition = parkedLocalPosition;
if (_obstacle != null) _obstacle.enabled = false;
}
}
}
Store/FloorPaint.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// 床の塗り分け(見た目だけ)。
///
/// Unity の NavMesh の表示は「通行者の中心が行ける場所」なので、見る人には
/// 「ロボットはこの細い帯しか通れない」と読めてしまう。人に見せる絵では
/// 「体が入れる床」を塗る。
///
/// 水色 :ロボットの体が入れる床
/// ロボット用 NavMesh(navAgentRadius で焼いたもの)の歩ける面を、
/// navAgentRadius だけ外へ広げた範囲。
/// 0.5 m より狭いすき間はそもそも NavMesh が無いので、塗られずに残る
/// 薄い黄色:人だけが入れる床
/// 人用 NavMesh(人の半径で焼いたもの)を人の半径だけ外へ広げた範囲から、
/// 水色の範囲を引いたところ
///
/// 塗りは当たりを持たない板(1色につき1枚のメッシュ)なので、
/// NavMesh には焼かれず、計算に使う NavMesh そのものは変わらない。
///
/// この script は見た目だけを扱う。店の形も人もロボットも動かさない。
/// </summary>
public class FloorPaint : MonoBehaviour
{
/// <summary>塗り分けの結果(報告に出す)</summary>
public struct Result
{
public float robotAreaM2; // ロボットの体が入れる床の広さ
public float peopleAreaM2; // 人だけが入れる床の広さ
public int robotQuads; // 塗りに使った四角の数
public int peopleQuads;
public float cellSize;
public bool ok;
public string note;
}
/// <summary>
/// 塗り分けを作り直す。
/// robotTri は navAgentRadius で焼いた面、peopleTri は人の半径で焼いた面。
/// </summary>
public static Result Rebuild(Transform parent, StoreLayout layout, Bounds bounds,
NavMeshTriangulation robotTri, NavMeshTriangulation peopleTri,
float personRadius, StorePlan plan,
Material robotMat, Material peopleMat)
{
Result res = new Result();
res.ok = false;
Clear(parent);
if (layout == null) { res.note = "StoreLayout がありません"; return res; }
float cell = Mathf.Max(0.02f, layout.floorPaintCellSize);
res.cellSize = cell;
float x0 = bounds.min.x, z0 = bounds.min.z;
int nx = Mathf.CeilToInt(bounds.size.x / cell) + 1;
int nz = Mathf.CeilToInt(bounds.size.z / cell) + 1;
if (nx <= 0 || nz <= 0 || (long)nx * nz > 4000000)
{
res.note = "塗る範囲が大きすぎます(floorPaintCellSize を大きくしてください)";
return res;
}
bool[] robotCore = Rasterize(robotTri, x0, z0, cell, nx, nz);
bool[] peopleCore = Rasterize(peopleTri, x0, z0, cell, nx, nz);
bool[] robot = Dilate(robotCore, nx, nz, layout.navAgentRadius / cell);
bool[] people = Dilate(peopleCore, nx, nz, personRadius / cell);
// 広げると壁や卓の中へはみ出す(壁の厚み 0.15 m より広げる分が大きいため)。
// 置いてあるもの(壁・卓・椅子・ベンチ・カウンター・厨房・事務所)の下は塗らない
bool[] blocked = RasterizeSolids(plan, x0, z0, cell, nx, nz);
if (blocked != null)
{
for (int i = 0; i < robot.Length; i++)
if (blocked[i]) { robot[i] = false; people[i] = false; }
}
// 人だけが入れる床 = 人の範囲 − ロボットの範囲
for (int i = 0; i < people.Length; i++) if (robot[i]) people[i] = false;
float y = Mathf.Max(0.001f, layout.floorPaintHeight);
res.robotQuads = Build(parent, "FloorPaint_Robot", robot, nx, nz, x0, z0, cell, y, robotMat);
res.peopleQuads = Build(parent, "FloorPaint_People", people, nx, nz, x0, z0, cell, y, peopleMat);
res.robotAreaM2 = Count(robot) * cell * cell;
res.peopleAreaM2 = Count(people) * cell * cell;
res.ok = true;
res.note = "ok";
return res;
}
/// <summary>塗りを消す</summary>
public static void Clear(Transform parent)
{
if (parent == null) return;
for (int i = parent.childCount - 1; i >= 0; i--)
{
Transform t = parent.GetChild(i);
if (!t.name.StartsWith("FloorPaint_")) continue;
if (Application.isPlaying) Destroy(t.gameObject);
else DestroyImmediate(t.gameObject);
}
}
// ---------------- 格子に落とす ----------------
/// <summary>歩ける三角形を格子に塗る(三角形ごとに、その囲み箱の中だけ見る)</summary>
static bool[] Rasterize(NavMeshTriangulation tri, float x0, float z0, float cell, int nx, int nz)
{
bool[] grid = new bool[nx * nz];
if (tri.indices == null || tri.vertices == null) return grid;
int triCount = tri.indices.Length / 3;
for (int t = 0; t < triCount; t++)
{
// Not Walkable(area 1)は NavMesh に入らないが、念のため外す
if (tri.areas != null && t < tri.areas.Length && tri.areas[t] == 1) continue;
Vector3 a = tri.vertices[tri.indices[t * 3]];
Vector3 b = tri.vertices[tri.indices[t * 3 + 1]];
Vector3 c = tri.vertices[tri.indices[t * 3 + 2]];
int ix0 = Mathf.Max(0, Mathf.FloorToInt((Mathf.Min(a.x, Mathf.Min(b.x, c.x)) - x0) / cell) - 1);
int ix1 = Mathf.Min(nx - 1, Mathf.CeilToInt((Mathf.Max(a.x, Mathf.Max(b.x, c.x)) - x0) / cell) + 1);
int iz0 = Mathf.Max(0, Mathf.FloorToInt((Mathf.Min(a.z, Mathf.Min(b.z, c.z)) - z0) / cell) - 1);
int iz1 = Mathf.Min(nz - 1, Mathf.CeilToInt((Mathf.Max(a.z, Mathf.Max(b.z, c.z)) - z0) / cell) + 1);
for (int iz = iz0; iz <= iz1; iz++)
{
float pz = z0 + (iz + 0.5f) * cell;
for (int ix = ix0; ix <= ix1; ix++)
{
int k = iz * nx + ix;
if (grid[k]) continue;
float px = x0 + (ix + 0.5f) * cell;
if (InTriangle(px, pz, a, b, c)) grid[k] = true;
}
}
}
return grid;
}
/// <summary>
/// 置いてあるもの(通れないもの)の footprint を格子に塗る。
/// 床・通路の色板・座る場所の見た目は通れるので入れない。
/// </summary>
static bool[] RasterizeSolids(StorePlan plan, float x0, float z0, float cell, int nx, int nz)
{
if (plan == null) return null;
bool[] g = new bool[nx * nz];
for (int i = 0; i < plan.boxes.Count; i++)
{
PlanBox b = plan.boxes[i];
if (b.style == BoxStyle.Floor || b.style == BoxStyle.AisleMark ||
b.style == BoxStyle.BenchSeat) continue;
Fill(g, b.rect, x0, z0, cell, nx, nz);
}
for (int i = 0; i < plan.places.Count; i++)
{
PlanPlace p = plan.places[i];
if (!p.solid || p.rect.IsEmpty) continue;
Fill(g, p.rect, x0, z0, cell, nx, nz);
}
return g;
}
static void Fill(bool[] g, PlanRect r, float x0, float z0, float cell, int nx, int nz)
{
int ix0 = Mathf.Max(0, Mathf.FloorToInt((r.xMin - x0) / cell));
int ix1 = Mathf.Min(nx - 1, Mathf.CeilToInt((r.xMax - x0) / cell) - 1);
int iz0 = Mathf.Max(0, Mathf.FloorToInt((r.zMin - z0) / cell));
int iz1 = Mathf.Min(nz - 1, Mathf.CeilToInt((r.zMax - z0) / cell) - 1);
for (int iz = iz0; iz <= iz1; iz++)
for (int ix = ix0; ix <= ix1; ix++) g[iz * nx + ix] = true;
}
static bool InTriangle(float px, float pz, Vector3 a, Vector3 b, Vector3 c)
{
float d1 = Side(px, pz, a, b);
float d2 = Side(px, pz, b, c);
float d3 = Side(px, pz, c, a);
bool neg = d1 < 0f || d2 < 0f || d3 < 0f;
bool pos = d1 > 0f || d2 > 0f || d3 > 0f;
return !(neg && pos);
}
static float Side(float px, float pz, Vector3 p, Vector3 q)
{
return (px - q.x) * (p.z - q.z) - (p.x - q.x) * (pz - q.z);
}
/// <summary>丸く広げる(半径はマスの数)</summary>
static bool[] Dilate(bool[] grid, int nx, int nz, float radiusCells)
{
int r = Mathf.FloorToInt(radiusCells + 0.5f);
if (r <= 0) return (bool[])grid.Clone();
// 広げる形(円)のマスの並びを先に作る
List<int> dx = new List<int>();
List<int> dz = new List<int>();
float r2 = radiusCells * radiusCells;
for (int j = -r; j <= r; j++)
for (int i = -r; i <= r; i++)
if (i * i + j * j <= r2 + 0.01f) { dx.Add(i); dz.Add(j); }
bool[] outGrid = new bool[grid.Length];
int n = dx.Count;
for (int iz = 0; iz < nz; iz++)
{
int row = iz * nx;
for (int ix = 0; ix < nx; ix++)
{
if (!grid[row + ix]) continue;
for (int k = 0; k < n; k++)
{
int jx = ix + dx[k];
if (jx < 0 || jx >= nx) continue;
int jz = iz + dz[k];
if (jz < 0 || jz >= nz) continue;
outGrid[jz * nx + jx] = true;
}
}
}
return outGrid;
}
static int Count(bool[] g)
{
int n = 0;
for (int i = 0; i < g.Length; i++) if (g[i]) n++;
return n;
}
// ---------------- 四角にまとめてメッシュにする ----------------
/// <summary>
/// 塗るマスを大きな四角にまとめて、1枚のメッシュにする。
/// 当たりは付けない(NavMesh に焼かれないようにする)。四角の数を返す。
/// </summary>
static int Build(Transform parent, string name, bool[] grid, int nx, int nz,
float x0, float z0, float cell, float y, Material mat)
{
bool[] used = new bool[grid.Length];
List<Vector3> verts = new List<Vector3>();
List<int> tris = new List<int>();
int quads = 0;
for (int iz = 0; iz < nz; iz++)
{
for (int ix = 0; ix < nx; ix++)
{
int k = iz * nx + ix;
if (!grid[k] || used[k]) continue;
// 右へ伸ばす
int w = 1;
while (ix + w < nx && grid[iz * nx + ix + w] && !used[iz * nx + ix + w]) w++;
// 上へ伸ばす(同じ幅ぶん続くあいだ)
int h = 1;
while (iz + h < nz)
{
bool full = true;
for (int i = 0; i < w; i++)
{
int kk = (iz + h) * nx + ix + i;
if (!grid[kk] || used[kk]) { full = false; break; }
}
if (!full) break;
h++;
}
for (int j = 0; j < h; j++)
for (int i = 0; i < w; i++) used[(iz + j) * nx + ix + i] = true;
float ax = x0 + ix * cell, az = z0 + iz * cell;
float bx = ax + w * cell, bz = az + h * cell;
int v = verts.Count;
verts.Add(new Vector3(ax, 0f, az));
verts.Add(new Vector3(bx, 0f, az));
verts.Add(new Vector3(bx, 0f, bz));
verts.Add(new Vector3(ax, 0f, bz));
tris.Add(v); tris.Add(v + 2); tris.Add(v + 1);
tris.Add(v); tris.Add(v + 3); tris.Add(v + 2);
quads++;
}
}
if (quads == 0) return 0;
GameObject go = new GameObject(name);
go.transform.SetParent(parent, false);
go.transform.position = new Vector3(0f, y, 0f);
Mesh mesh = new Mesh();
mesh.name = name;
mesh.indexFormat = verts.Count > 65000
? UnityEngine.Rendering.IndexFormat.UInt32
: UnityEngine.Rendering.IndexFormat.UInt16;
mesh.SetVertices(verts);
mesh.SetTriangles(tris, 0);
mesh.RecalculateNormals();
mesh.RecalculateBounds();
go.AddComponent<MeshFilter>().sharedMesh = mesh;
MeshRenderer mr = go.AddComponent<MeshRenderer>();
if (mat != null) mr.sharedMaterial = mat;
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
mr.receiveShadows = false;
return quads;
}
}
}
Store/GridStoreLayout.cs
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 最初に作った「左右振り分け型」の店(同じ卓を格子に並べ、細い通路が枝分かれする形)。
/// 比べる用に残してある。StoreLayout_Grid.asset がこれ。
///
/// 数字はすべてこの .asset に置く。
/// </summary>
[CreateAssetMenu(menuName = "ServiceSim/Store Layout (Grid)", fileName = "StoreLayout_Grid")]
public class GridStoreLayout : StoreLayout
{
[Header("卓の並び")]
[Tooltip("置く卓の数。スロット数より少ない分はドリンクバーなどに使う")]
public int tableCount = 35;
[Tooltip("太い通路から枝分かれする細い通路の中心位置(Z, m)")]
public float[] branchAisleCenterZ = { 4.9f, 9.3f, 13.7f };
[Tooltip("1本の細い通路の片側に並べる卓の数。通路の南北2列ぶんが置かれる")]
public int unitsPerBranchSide = 6;
[Tooltip("卓の並ぶ間隔(X, m)")]
public float unitPitchX = 1.4f;
[Tooltip("太い通路にいちばん近い卓の中心位置(X, m)")]
public float firstUnitCenterX = 1.35f;
[Tooltip("ドリンクバーに使うスロット番号(0 起点)")]
public int drinkBarSlotIndex = 35;
[Header("通路")]
[Tooltip("厨房から伸びる太い通路の幅(m)")]
public float mainAisleWidth = 1.5f;
[Tooltip("奥(東の端)に、3本の細い通路をつなぐ横通路を作るか")]
public bool rearCrossAisle = false;
[Header("卓と椅子(箱で作る)")]
public Vector3 tableSize = new Vector3(1.2f, 0.72f, 0.8f);
public Vector3 chairSize = new Vector3(0.45f, 0.45f, 0.4f);
[Tooltip("卓の中心から椅子の中心までの横ずれ(X, m)。1卓に4脚")]
public float chairOffsetX = 0.3f;
[Tooltip("卓と卓のあいだの仕切り板の高さ(m)")]
public float boothDividerHeight = 1.2f;
[Header("厨房の受け渡し口")]
public Vector3 passCounterSize = new Vector3(1.6f, 1.0f, 0.5f);
[Tooltip("ロボットが料理を受け取る位置(通路上, m)")]
public Vector3 passPoint = new Vector3(0f, 0f, 0.15f);
[Header("レジ・トイレ・ドリンクバー")]
public Vector3 registerCounterSize = new Vector3(2.0f, 1.0f, 0.6f);
public float registerCenterX = 2.4f;
public Vector3 toiletBlockSize = new Vector3(2.4f, 2.4f, 1.6f);
public float toiletCenterX = 7.6f;
[Tooltip("レジ・トイレの手前に取る、人が立つ余地(m)")]
public float poiApproachGap = 0.7f;
[Tooltip("ドリンクバーの高さ(m)")]
public float drinkBarHeight = 1.1f;
[Header("手前の広場")]
[Tooltip("受け渡し口の前に取る広場の奥行き(m)。レジ・トイレ・待合に使う")]
public float frontPocketDepth = 3.4f;
public override string StoreKindName { get { return "格子型(比べる用)"; } }
// ---- 計算で出る値 ----
float UnitDepth { get { return chairSize.z * 2f + tableSize.z; } }
int BranchCount { get { return branchAisleCenterZ != null ? branchAisleCenterZ.Length : 0; } }
int SlotCount { get { return BranchCount * 2 * unitsPerBranchSide; } }
float UnitCenterX(int i) { return firstUnitCenterX + i * unitPitchX; }
float TableBlockMaxX { get { return UnitCenterX(unitsPerBranchSide - 1) + tableSize.x * 0.5f; } }
float InteriorMinX { get { return -mainAisleWidth * 0.5f; } }
float InteriorMaxX { get { return TableBlockMaxX + (rearCrossAisle ? MaxAisleWidth : 0f); } }
float InteriorMinZ
{
get { return BranchCount == 0 ? 0f : branchAisleCenterZ[0] - MaxAisleWidth * 0.5f - UnitDepth - frontPocketDepth; }
}
float InteriorMaxZ
{
get { return BranchCount == 0 ? 0f : branchAisleCenterZ[BranchCount - 1] + MaxAisleWidth * 0.5f + UnitDepth; }
}
static float SideSign(int side) { return side == 0 ? -1f : 1f; }
protected override StorePlan Compose()
{
StorePlan plan = new StorePlan();
if (BranchCount == 0 || tableCount > SlotCount) return plan;
float w = AisleWidth;
float unitDepth = UnitDepth;
float h = wallHeight;
float x0 = InteriorMinX, x1 = InteriorMaxX;
float z0 = InteriorMinZ, z1 = InteriorMaxZ;
float eastX0 = mainAisleWidth * 0.5f;
float tableMaxX = TableBlockMaxX;
float off = DeliveryOffset();
plan.AddFloor(new PlanRect(x0, z0, x1, z1));
// 壁
float t = wallThickness;
plan.AddBox("Wall_South", BoxStyle.Wall, new PlanRect(x0 - t, z0 - t, x1 + t, z0), h);
plan.AddBox("Wall_North", BoxStyle.Wall, new PlanRect(x0 - t, z1, x1 + t, z1 + t), h);
plan.AddBox("Wall_West", BoxStyle.Wall, new PlanRect(x0 - t, z0 - t, x0, z1 + t), h);
plan.AddBox("Wall_East", BoxStyle.Wall, new PlanRect(x1, z0 - t, x1 + t, z1 + t), h);
// 受け渡し口のカウンター
plan.AddPlace(PlaceKind.FoodPass, "PassCounter",
PlanRect.FromSize(x0, z0, passCounterSize.x, passCounterSize.z),
passCounterSize.y, passPoint, true);
// 卓の列
int nextTableId = 1;
for (int k = 0; k < BranchCount; k++)
{
float zc = branchAisleCenterZ[k];
for (int side = 0; side < 2; side++)
{
float sign = SideSign(side);
float rowNear = zc + sign * (w * 0.5f);
float rowFar = zc + sign * (w * 0.5f + unitDepth);
for (int u = 0; u < unitsPerBranchSide; u++)
{
int slot = k * 2 * unitsPerBranchSide + side * unitsPerBranchSide + u;
float cx = UnitCenterX(u);
if (slot == drinkBarSlotIndex || nextTableId > tableCount)
{
plan.AddPlace(PlaceKind.DrinkBar, "DrinkBar",
new PlanRect(cx - tableSize.x * 0.5f, Mathf.Min(rowNear, rowFar),
cx + tableSize.x * 0.5f, Mathf.Max(rowNear, rowFar)),
drinkBarHeight, new Vector3(cx, 0f, zc + sign * off), true);
continue;
}
float tz = zc + sign * (w * 0.5f + chairSize.z + tableSize.z * 0.5f);
PlanSeat seat = new PlanSeat();
seat.tableId = nextTableId++;
seat.kind = SeatKind.IslandTable;
seat.tableRect = PlanRect.FromCenter(new Vector3(cx, 0f, tz), tableSize.x, tableSize.z);
seat.tableHeight = tableSize.y;
seat.deliveryPoint = new Vector3(cx, 0f, zc + sign * off);
seat.aisleIndex = 1 + k; // 0 は太い通路
float chairZ = tableSize.z * 0.5f + chairSize.z * 0.5f;
for (int sx = -1; sx <= 1; sx += 2)
{
for (int sz = -1; sz <= 1; sz += 2)
{
PlanChair chair = new PlanChair();
chair.rect = PlanRect.FromCenter(
new Vector3(cx + sx * chairOffsetX, 0f, tz + sz * chairZ),
chairSize.x, chairSize.z);
chair.height = chairSize.y;
chair.pullable = (sz < 0) == (sign > 0);
chair.pullDirection = new Vector3(0f, 0f, -sign);
seat.chairs.Add(chair);
}
}
plan.seats.Add(seat);
}
// 卓と卓のあいだの仕切り板
for (int u = 0; u < unitsPerBranchSide - 1; u++)
{
float gx0 = UnitCenterX(u) + tableSize.x * 0.5f;
float gx1 = UnitCenterX(u + 1) - tableSize.x * 0.5f;
plan.AddBox("BoothDivider", BoxStyle.Divider,
new PlanRect(gx0, Mathf.Min(rowNear, rowFar), gx1, Mathf.Max(rowNear, rowFar)),
boothDividerHeight);
}
}
}
// 通路以外のすき間をふさぐ
for (int k = 0; k < BranchCount - 1; k++)
{
float za = branchAisleCenterZ[k] + w * 0.5f + unitDepth;
float zb = branchAisleCenterZ[k + 1] - w * 0.5f - unitDepth;
plan.AddBox("GapBetweenBranch", BoxStyle.Wall, new PlanRect(eastX0, za, tableMaxX, zb), h);
}
float zn = branchAisleCenterZ[BranchCount - 1] + w * 0.5f + unitDepth;
plan.AddBox("NorthFiller", BoxStyle.Wall, new PlanRect(eastX0, zn, tableMaxX, z1), h);
float zm = branchAisleCenterZ[BranchCount - 1] + w * 0.5f;
plan.AddBox("MainAisleEnd", BoxStyle.Wall, new PlanRect(x0, zm, eastX0, z1), h);
if (rearCrossAisle)
{
float cx0 = tableMaxX, cx1 = x1, crossEast = tableMaxX + w;
float zS = branchAisleCenterZ[0] - w * 0.5f;
float zN = branchAisleCenterZ[BranchCount - 1] + w * 0.5f;
plan.AddBox("RearSouthFiller", BoxStyle.Wall, new PlanRect(cx0, z0, cx1, zS), h);
plan.AddBox("RearNorthFiller", BoxStyle.Wall, new PlanRect(cx0, zN, cx1, z1), h);
plan.AddBox("RearEastFiller", BoxStyle.Wall, new PlanRect(crossEast, zS, cx1, zN), h);
}
// レジ・トイレ
plan.AddPlace(PlaceKind.Register, "RegisterCounter",
new PlanRect(registerCenterX - registerCounterSize.x * 0.5f, z0,
registerCenterX + registerCounterSize.x * 0.5f, z0 + registerCounterSize.z),
registerCounterSize.y,
new Vector3(registerCenterX, 0f, z0 + registerCounterSize.z + poiApproachGap), true);
plan.AddPlace(PlaceKind.Toilet, "ToiletBlock",
new PlanRect(toiletCenterX - toiletBlockSize.x * 0.5f, z0,
toiletCenterX + toiletBlockSize.x * 0.5f, z0 + toiletBlockSize.z),
toiletBlockSize.y,
new Vector3(toiletCenterX, 0f, z0 + toiletBlockSize.z + poiApproachGap), true);
// 通路(0 番は太い通路)
plan.AddAisle("MainAisle", new PlanRect(x0, z0, eastX0, zm), false, passPoint);
for (int k = 0; k < BranchCount; k++)
{
float zc = branchAisleCenterZ[k];
plan.AddAisle("BranchAisle" + (k + 1).ToString("00"),
new PlanRect(eastX0, zc - w * 0.5f, tableMaxX, zc + w * 0.5f), true,
new Vector3(-aisleEntranceSideOffset, 0f, zc - aisleEntranceSetback));
}
if (rearCrossAisle)
{
plan.AddAisle("RearCrossAisle",
new PlanRect(tableMaxX, branchAisleCenterZ[0] - w * 0.5f,
tableMaxX + w, branchAisleCenterZ[BranchCount - 1] + w * 0.5f),
false, passPoint);
}
return plan;
}
}
}
Store/NavMeshBaker.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// 店の形から NavMesh を焼く。com.unity.ai.navigation を入れずに、
/// 組み込みの UnityEngine.AI の低レベル API だけで焼く(NavMeshSurface の代わり)。
///
/// 焼く対象は bakeRoot の下だけ。人とロボットは別の親の下に置くので、
/// 障害物として焼き込まれない。
/// 数字(半径・高さ・細かさ)は StoreLayout の .asset に置く。
/// </summary>
public class NavMeshBaker : MonoBehaviour
{
[Header("設定")]
public StoreLayout layout;
[Tooltip("この下にあるコライダーだけを焼く。ふつうは Store の親")]
public Transform bakeRoot;
[Tooltip("焼く範囲を、組み立てた店の広さから取る")]
public StoreBuilder store;
[Tooltip("人用の NavMesh を焼く半径(m)。PeopleSpec の人の半径に合わせる。" +
"0 以下、または navAgentRadius 以上なら人用は焼かず、人もロボット用の面を歩く")]
public float peopleRadius = 0.2f;
[Tooltip("レジの内側(店員側)に付ける NavMesh の区画の番号。PeopleSpec に合わせる。" +
"客の NavMeshAgent はこの区画を外すので、店員側に入れない。0 なら区切らない")]
public int registerBackArea = 3;
NavMeshData _data;
NavMeshDataInstance _instance;
// ---- 人用の NavMesh(半径が人の分だけ小さい)----
// ロボット用(navAgentRadius)と人用(人の半径)を同時に置き、
// それぞれの通行者の種類(agentTypeID)で引き分ける。
NavMeshData _peopleData;
NavMeshDataInstance _peopleInstance;
int _peopleTypeId = -1;
/// <summary>ロボット用の通行者の種類(既定の 0)</summary>
public int RobotAgentTypeId { get { return 0; } }
/// <summary>人用の通行者の種類。焼いていなければロボット用と同じ</summary>
public int PeopleAgentTypeId { get { return _peopleTypeId >= 0 ? _peopleTypeId : 0; } }
/// <summary>人用の NavMesh を置けているか</summary>
public bool HasPeopleNavMesh { get { return _peopleInstance.valid; } }
/// <summary>人用の NavMesh を焼いた半径(m)</summary>
public float PeopleRadius { get { return peopleRadius; } }
/// <summary>その種類で経路を引くための絞り込み</summary>
public NavMeshQueryFilter FilterFor(int agentTypeId)
{
NavMeshQueryFilter f = new NavMeshQueryFilter();
f.agentTypeID = agentTypeId;
f.areaMask = NavMesh.AllAreas;
return f;
}
readonly List<NavMeshBuildSource> _sources = new List<NavMeshBuildSource>();
readonly List<NavMeshBuildMarkup> _markups = new List<NavMeshBuildMarkup>();
public bool HasNavMesh { get { return _instance.valid; } }
void OnDisable()
{
Remove();
}
/// <summary>
/// いまの店の形で焼き直す。人の半径を渡すと、人用の NavMesh も一緒に焼く。
/// 人の半径が 0 以下、またはロボットの半径以上なら、人用は焼かない(1枚だけ)。
/// </summary>
public void Bake(float radius)
{
peopleRadius = radius;
Bake();
}
/// <summary>いまの店の形で焼き直す</summary>
public void Bake()
{
if (layout == null)
{
Debug.LogError("[ServiceSim] NavMeshBaker に StoreLayout が入っていません");
return;
}
Transform root = bakeRoot != null ? bakeRoot : transform;
_sources.Clear();
BuildMarkups();
NavMeshBuilder.CollectSources(
root,
~0, // すべてのレイヤー(レイヤーを増やさないため)
NavMeshCollectGeometry.PhysicsColliders, // コライダーの形で焼く
0, // 既定のエリア(Walkable)
_markups,
_sources);
AddRegisterBackArea();
if (_sources.Count == 0)
{
Debug.LogError("[ServiceSim] 焼く形が見つかりません。先に Build Store を実行してください");
return;
}
NavMeshBuildSettings settings = BuildSettings();
Bounds bounds = BakeBounds();
Remove();
_data = NavMeshBuilder.BuildNavMeshData(settings, _sources, bounds, Vector3.zero, Quaternion.identity);
if (_data == null)
{
Debug.LogError("[ServiceSim] NavMesh を焼けませんでした");
return;
}
_data.name = "ServiceSim_NavMesh";
_instance = NavMesh.AddNavMeshData(_data);
BakePeople(settings, bounds);
}
/// <summary>
/// 人用の NavMesh を焼いて一緒に置く(半径だけが違う)。
/// 通行者の種類を1つ作り、人の NavMeshAgent にその番号を入れて使い分ける。
/// ロボットはこれまでどおり navAgentRadius の面を歩く。
/// </summary>
void BakePeople(NavMeshBuildSettings robotSettings, Bounds bounds)
{
RemovePeople();
if (peopleRadius <= 0.0005f || peopleRadius >= layout.navAgentRadius - 0.0005f) return;
if (_peopleTypeId < 0)
{
NavMeshBuildSettings made = NavMesh.CreateSettings();
_peopleTypeId = made.agentTypeID;
}
NavMeshBuildSettings s = robotSettings;
s.agentTypeID = _peopleTypeId;
s.agentRadius = peopleRadius;
_peopleData = NavMeshBuilder.BuildNavMeshData(s, _sources, bounds,
Vector3.zero, Quaternion.identity);
if (_peopleData == null)
{
Debug.LogWarning("[ServiceSim] 人用の NavMesh を焼けませんでした。人もロボット用の面を歩きます");
return;
}
_peopleData.name = "ServiceSim_NavMesh_People";
_peopleInstance = NavMesh.AddNavMeshData(_peopleData);
PeopleNav.SetPeopleAgentType(_peopleTypeId);
}
void RemovePeople()
{
PeopleNav.SetPeopleAgentType(0);
if (_peopleInstance.valid) NavMesh.RemoveNavMeshData(_peopleInstance);
_peopleInstance = new NavMeshDataInstance();
_peopleData = null;
}
/// <summary>焼いた NavMesh を外す</summary>
public void Remove()
{
if (_instance.valid) NavMesh.RemoveNavMeshData(_instance);
_instance = new NavMeshDataInstance();
_data = null;
RemovePeople();
}
/// <summary>
/// 壁・ベンチ・仕切り・卓・椅子・カウンターを Not Walkable にする印を作る。
/// 焼く形としては残るので通れないままだが、上面は歩ける場所にならない。
/// </summary>
void BuildMarkups()
{
_markups.Clear();
if (store == null) return;
int notWalkable = NavMesh.GetAreaFromName("Not Walkable");
if (notWalkable < 0) notWalkable = 1;
IList<Transform> roots = store.NotWalkableRoots;
for (int i = 0; i < roots.Count; i++)
{
if (roots[i] == null) continue;
NavMeshBuildMarkup m = new NavMeshBuildMarkup();
m.root = roots[i];
m.overrideArea = true;
m.area = notWalkable;
_markups.Add(m);
}
}
/// <summary>焼く範囲。組み立てた店の広さに余白を足す</summary>
public Bounds BakeBounds()
{
if (store != null && store.Plan != null)
return store.Plan.NavBounds(layout.navBoundsPadding);
// 店がまだ組み立てられていないときは、この下のコライダーの広がりを使う
Transform root = bakeRoot != null ? bakeRoot : transform;
Collider[] cols = root.GetComponentsInChildren<Collider>(false);
if (cols.Length == 0) return new Bounds(Vector3.zero, new Vector3(20f, 4f, 20f));
Bounds b = cols[0].bounds;
for (int i = 1; i < cols.Length; i++) b.Encapsulate(cols[i].bounds);
b.Expand(layout.navBoundsPadding * 2f);
return b;
}
/// <summary>
/// レジの内側(店員側)を別の区画にする。
/// 形は足さず、その箱の中の区画の番号だけを変える(ModifierBox)。
/// 客の NavMeshAgent がこの区画を外して歩くので、客は店員側に入れない。
/// </summary>
void AddRegisterBackArea()
{
if (registerBackArea <= 2) return; // 0・1・2 は決まった区画なので使わない
if (store == null) return;
PlanRect r = store.RegisterBackRect;
if (r.IsEmpty) return;
NavMeshBuildSource src = new NavMeshBuildSource();
src.shape = NavMeshBuildSourceShape.ModifierBox;
src.transform = Matrix4x4.Translate(new Vector3(r.CenterX, 0.5f, r.CenterZ));
src.size = new Vector3(r.SizeX, 2f, r.SizeZ);
src.area = registerBackArea;
_sources.Add(src);
}
/// <summary>
/// 通行半径を変えて焼き直し、その歩ける面の三角形を返す(見た目の塗り分けに使う)。
/// いま入れてある NavMesh を外してから焼き、読み終わったら元に戻すので、
/// 計算に使う NavMesh(navAgentRadius で焼いたもの)は変わらない。
/// </summary>
public NavMeshTriangulation TriangulateFor(float agentRadius)
{
NavMeshTriangulation empty = new NavMeshTriangulation();
if (layout == null) return empty;
Transform root = bakeRoot != null ? bakeRoot : transform;
_sources.Clear();
BuildMarkups();
NavMeshBuilder.CollectSources(root, ~0, NavMeshCollectGeometry.PhysicsColliders,
0, _markups, _sources);
AddRegisterBackArea();
if (_sources.Count == 0) return empty;
NavMeshBuildSettings s = BuildSettings();
s.agentRadius = agentRadius;
// いまの NavMesh を一時的に全部外す(CalculateTriangulation は入れてある全部を返すため)
bool had = _instance.valid;
NavMeshData keep = _data;
if (had) NavMesh.RemoveNavMeshData(_instance);
_instance = new NavMeshDataInstance();
bool hadPeople = _peopleInstance.valid;
NavMeshData keepPeople = _peopleData;
if (hadPeople) NavMesh.RemoveNavMeshData(_peopleInstance);
_peopleInstance = new NavMeshDataInstance();
NavMeshTriangulation tri = empty;
NavMeshData tmp = NavMeshBuilder.BuildNavMeshData(
s, _sources, BakeBounds(), Vector3.zero, Quaternion.identity);
NavMeshDataInstance tmpInst = new NavMeshDataInstance();
if (tmp != null)
{
tmpInst = NavMesh.AddNavMeshData(tmp);
tri = NavMesh.CalculateTriangulation();
}
if (tmpInst.valid) NavMesh.RemoveNavMeshData(tmpInst);
// 元に戻す
_data = keep;
if (had && keep != null) _instance = NavMesh.AddNavMeshData(keep);
_peopleData = keepPeople;
if (hadPeople && keepPeople != null) _peopleInstance = NavMesh.AddNavMeshData(keepPeople);
return tri;
}
NavMeshBuildSettings BuildSettings()
{
NavMeshBuildSettings s = NavMesh.GetSettingsByID(0);
s.agentRadius = layout.navAgentRadius;
s.agentHeight = layout.navAgentHeight;
s.agentClimb = layout.navAgentClimb;
s.agentSlope = layout.navAgentSlope;
s.overrideVoxelSize = true;
s.voxelSize = layout.navVoxelSize;
s.minRegionArea = layout.navMinRegionArea;
return s;
}
/// <summary>
/// 受け渡し口から1点までの NavMesh 上の道のり(m)。
/// つながっていなければ -1 を返す。
/// </summary>
public static float PathLength(Vector3 from, Vector3 to, float sampleRadius, out string status)
{
NavMeshQueryFilter f = new NavMeshQueryFilter();
f.agentTypeID = 0;
f.areaMask = NavMesh.AllAreas;
return PathLength(from, to, sampleRadius, f, out status);
}
/// <summary>通行者の種類を指定して道のりを測る(人用・ロボット用を分けて確かめる)</summary>
public static float PathLength(Vector3 from, Vector3 to, float sampleRadius,
NavMeshQueryFilter filter, out string status)
{
NavMeshHit hitFrom, hitTo;
if (!NavMesh.SamplePosition(from, out hitFrom, sampleRadius, filter))
{
status = "出発点が NavMesh の上にない";
return -1f;
}
if (!NavMesh.SamplePosition(to, out hitTo, sampleRadius, filter))
{
status = "行き先が NavMesh の上にない";
return -1f;
}
NavMeshPath path = new NavMeshPath();
if (!NavMesh.CalculatePath(hitFrom.position, hitTo.position, filter, path))
{
status = "経路が見つからない";
return -1f;
}
if (path.status != NavMeshPathStatus.PathComplete)
{
status = path.status == NavMeshPathStatus.PathPartial ? "経路が途中まで" : "経路が無効";
return -1f;
}
float len = 0f;
Vector3[] c = path.corners;
for (int i = 1; i < c.Length; i++) len += Vector3.Distance(c[i - 1], c[i]);
status = "ok";
return len;
}
void OnDrawGizmosSelected()
{
if (layout == null) return;
Bounds b = BakeBounds();
Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.5f);
Gizmos.DrawWireCube(b.center, b.size);
}
}
}
Store/PlaceNameLabels.cs
using System.Collections.Generic;
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 場所の名前を床の上に出す(見た目だけ)。
///
/// 頭の上の字幕(ServiceSimLabels)は Scene ビューにだけ描くのでカメラに写らない。
/// 場所の名前は真上の画面にも写したいので、文字の板(TextMesh)で置く。
/// 当たりは付けないので NavMesh には焼かれない。
///
/// ・横書き。真上のカメラから見て、上が北の向きで読める
/// ・1つの場所に1つだけ。短い名前
/// ・文字の大きさはそろえる
/// ・場所の中に収まらなければ、場所の外に置いて細い線でつなぐ
/// ・名前どうしが重ならないよう、空いているほうへずらす
///
/// 計測の回では出さない(見た目だけのものなので、速さを落とさないため)。
/// Build Store と「見る用」の回でだけ作る。
///
/// この script は見た目だけを扱う。店の形も人もロボットも動かさない。
/// </summary>
public static class PlaceNameLabels
{
const string RootName = "PlaceNames";
/// <summary>1つぶんの名前(出す場所と、つなぎ先)</summary>
struct Item
{
public string text;
public PlanRect area; // その場所の広がり(空なら点だけ)
public Vector3 anchor; // つなぎ先(場所の中心)
}
/// <summary>置いた結果(報告に出す)</summary>
public struct Result
{
public int count; // 出した名前の数
public int moved; // 重なりを避けてずらした数
public int overlaps; // それでも重なっている数(0 になること)
public int leaders; // 場所の外に出して線でつないだ数
}
/// <summary>
/// 真上から見て読める名前を作り直す。
/// 短い名前の対応表はここに持つ(場所の種類 → 出す名前)。
/// </summary>
public static Result Rebuild(StoreBuilder builder)
{
Result res = new Result();
if (builder == null) return res;
Transform root = Root(builder.transform);
ClearChildren(root);
StoreLayout layout = builder.layout;
StorePlan plan = builder.Plan;
if (layout == null || plan == null || !layout.showPlaceNames) return res;
List<Item> items = Collect(builder, plan);
float size = Mathf.Max(0.04f, layout.placeNameSize);
List<Rect> taken = new List<Rect>();
for (int i = 0; i < items.Count; i++)
{
Item it = items[i];
// 文字の囲みの大きさ(日本語は1文字ぶんの幅で見積もる)
float w = it.text.Length * size * 1.05f;
float h = size * 1.3f;
// 場所の中に収まるか。収まらなければ外(北か南の空いているほう)へ
bool outside = it.area.IsEmpty || it.area.SizeX < w || it.area.SizeZ < h;
Vector2 at = new Vector2(it.anchor.x, it.anchor.z);
if (outside)
{
at.y = it.area.IsEmpty ? it.anchor.z + h : it.area.zMax + h * 0.9f;
res.leaders++;
}
// 重なりを避けて、north→south→east→west の順に少しずつずらす
Rect r = new Rect(at.x - w * 0.5f, at.y - h * 0.5f, w, h);
Vector2 moved;
bool ok = Avoid(r, taken, h, w, out moved);
if (moved != at) res.moved++;
if (!ok) res.overlaps++;
at = moved;
r = new Rect(at.x - w * 0.5f, at.y - h * 0.5f, w, h);
taken.Add(r);
Label(root, it.text, at, layout, size);
if (outside || Vector2.Distance(at, new Vector2(it.anchor.x, it.anchor.z)) > h)
Leader(root, at, new Vector2(it.anchor.x, it.anchor.z), layout);
res.count++;
}
return res;
}
public static void Clear(StoreBuilder builder)
{
if (builder == null) return;
ClearChildren(Root(builder.transform));
}
// ---------------- 出す名前を集める(1つの場所に1つだけ)----------------
static List<Item> Collect(StoreBuilder builder, StorePlan plan)
{
// 出す場所と、短い名前。ここに無い種類(事務所・ドリンクバーの前/列・レジの列)は出さない
PlaceKind[] kinds =
{
PlaceKind.Entrance, PlaceKind.Vestibule, PlaceKind.Waiting,
PlaceKind.Register, PlaceKind.RegisterBack, PlaceKind.Kitchen,
PlaceKind.FoodPass, PlaceKind.DishReturn, PlaceKind.Pantry,
PlaceKind.DrinkBar, PlaceKind.Toilet
};
string[] names =
{
"入口", "風除室", "待合",
"レジ", "レジの内側", "厨房",
"受け渡し口", "返却口", "パントリー",
"ドリンクバー", "トイレ"
};
List<Item> list = new List<Item>();
for (int k = 0; k < kinds.Length; k++)
{
// 同じ種類が2つ以上あっても、広がりをまとめて1つだけ出す
PlanRect area = new PlanRect(0f, 0f, 0f, 0f);
Vector3 point = Vector3.zero;
bool found = false, hasArea = false;
for (int i = 0; i < plan.places.Count; i++)
{
PlanPlace p = plan.places[i];
if (p.kind != kinds[k]) continue;
if (!found) { point = p.point; found = true; }
if (p.rect.IsEmpty) continue;
area = hasArea ? Union(area, p.rect) : p.rect;
hasArea = true;
}
if (!found) continue;
Item it = new Item();
it.text = names[k];
it.area = hasArea ? area : new PlanRect(0f, 0f, 0f, 0f);
it.anchor = hasArea ? area.Center : point;
list.Add(it);
}
// 待機の点は「◯◯待機×2」とまとめて1つだけ
AddStandby(list, builder.RobotStandbyPoints, "ロボット待機");
AddStandby(list, builder.StaffStandbyPoints, "スタッフ待機");
return list;
}
static void AddStandby(List<Item> list, IList<Vector3> points, string name)
{
if (points == null || points.Count == 0) return;
Vector3 sum = Vector3.zero;
for (int i = 0; i < points.Count; i++) sum += points[i];
Item it = new Item();
it.text = points.Count > 1 ? name + "×" + points.Count : name;
it.area = new PlanRect(0f, 0f, 0f, 0f);
it.anchor = sum / points.Count;
list.Add(it);
}
static PlanRect Union(PlanRect a, PlanRect b)
{
return new PlanRect(Mathf.Min(a.xMin, b.xMin), Mathf.Min(a.zMin, b.zMin),
Mathf.Max(a.xMax, b.xMax), Mathf.Max(a.zMax, b.zMax));
}
// ---------------- 重なりを避ける ----------------
/// <summary>
/// すでに置いた囲みと重なるなら、北・南・東・西へ少しずつずらして空きを探す。
/// 見つかれば true。見つからなければ元の位置のまま false。
/// </summary>
static bool Avoid(Rect r, List<Rect> taken, float h, float w, out Vector2 at)
{
Vector2 start = r.center;
at = start;
if (!Hits(r, taken)) return true;
Vector2[] dirs =
{
new Vector2(0f, 1f), new Vector2(0f, -1f),
new Vector2(1f, 0f), new Vector2(-1f, 0f)
};
for (int step = 1; step <= 8; step++)
{
for (int d = 0; d < dirs.Length; d++)
{
Vector2 c = start + new Vector2(dirs[d].x * w * 0.6f * step,
dirs[d].y * h * 1.15f * step);
Rect t = new Rect(c.x - w * 0.5f, c.y - h * 0.5f, w, h);
if (Hits(t, taken)) continue;
at = c;
return true;
}
}
return false;
}
static bool Hits(Rect r, List<Rect> taken)
{
for (int i = 0; i < taken.Count; i++) if (r.Overlaps(taken[i])) return true;
return false;
}
// ---------------- 置く ----------------
static Transform Root(Transform parent)
{
Transform t = parent.Find(RootName);
if (t == null)
{
GameObject go = new GameObject(RootName);
go.transform.SetParent(parent, false);
t = go.transform;
}
return t;
}
static void ClearChildren(Transform root)
{
if (root == null) return;
for (int i = root.childCount - 1; i >= 0; i--)
{
GameObject go = root.GetChild(i).gameObject;
if (Application.isPlaying) Object.Destroy(go);
else Object.DestroyImmediate(go);
}
}
static void Label(Transform root, string text, Vector2 at, StoreLayout layout, float size)
{
GameObject go = new GameObject("PlaceName_" + text);
go.transform.SetParent(root, false);
go.transform.position = new Vector3(at.x, layout.placeNameHeight, at.y);
// 真上から見て横書き・上が北。
// 文字の右(+X)→ 東、文字の上(+Y)→ 北(+Z)になる向き
go.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
TextMesh tm = go.AddComponent<TextMesh>();
tm.text = text;
tm.anchor = TextAnchor.MiddleCenter;
tm.alignment = TextAlignment.Center;
tm.characterSize = size; // 大きさは全部そろえる
tm.fontSize = 64;
tm.lineSpacing = 1f;
tm.richText = false;
tm.color = new Color(0.08f, 0.09f, 0.12f);
Font font = JapaneseFont();
if (font != null) tm.font = font;
MeshRenderer mr = go.GetComponent<MeshRenderer>();
if (mr == null) return;
Material mat = font != null ? font.material : null;
if (mat == null)
{
Shader sh = Shader.Find("GUI/Text Shader");
if (sh != null) mat = new Material(sh);
}
if (mat != null) mr.sharedMaterial = mat;
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
mr.receiveShadows = false;
}
/// <summary>名前と場所をつなぐ細い線(当たりなし)</summary>
static void Leader(Transform root, Vector2 from, Vector2 to, StoreLayout layout)
{
Vector2 d = to - from;
float len = d.magnitude;
if (len < 0.05f) return;
GameObject go = GameObject.CreatePrimitive(PrimitiveType.Quad);
go.name = "PlaceName_Leader";
Object.DestroyImmediate(go.GetComponent<Collider>());
go.transform.SetParent(root, false);
go.transform.position = new Vector3((from.x + to.x) * 0.5f,
layout.placeNameHeight - 0.005f,
(from.y + to.y) * 0.5f);
float deg = -Mathf.Atan2(d.y, d.x) * Mathf.Rad2Deg;
go.transform.rotation = Quaternion.Euler(90f, deg, 0f);
go.transform.localScale = new Vector3(len, 0.02f, 1f);
MeshRenderer mr = go.GetComponent<MeshRenderer>();
Shader sh = Shader.Find("GUI/Text Shader");
if (sh != null)
{
Material m = new Material(sh);
m.color = new Color(0.25f, 0.27f, 0.32f);
mr.sharedMaterial = m;
}
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
mr.receiveShadows = false;
}
static Font _font;
static bool _tried;
/// <summary>日本語が出るフォント(組み込みの Arial には日本語が無い)</summary>
static Font JapaneseFont()
{
if (_tried) return _font;
_tried = true;
string[] names =
{
"Hiragino Sans", "HiraginoSans-W3", "Hiragino Kaku Gothic ProN",
"Yu Gothic", "YuGothic", "Noto Sans CJK JP", "Arial Unicode MS"
};
try { _font = Font.CreateDynamicFontFromOSFont(names, 64); }
catch { _font = null; }
return _font;
}
}
}
Store/StoreBuilder.cs
using System;
using System.Collections.Generic;
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// StoreLayout が作った部品の一覧(StorePlan)を見て、床・壁・卓・椅子・ベンチ・
/// カウンター・通路の色板を並べる。
///
/// この script は店だけを扱う。ロボットと人は動かさない。
/// 格子や帯の組み方は知らない(StorePlan だけを見る)。
/// </summary>
public class StoreBuilder : MonoBehaviour
{
[Serializable]
public class TableInfo
{
public int tableId;
public SeatKind kind;
public Transform root;
public Vector3 center;
public Vector3 deliveryPoint; // ロボットが止まる位置(通路の上)
public Vector3 aisleEntrance; // その通路に入る前に待つ位置
public Bounds aisleZone; // 譲り合いの区画
public int aisleIndex = -1;
/// <summary>何人かけの卓か(2人席・4人席)</summary>
public int seatCount = 4;
public List<Chair> aisleChairs = new List<Chair>();
/// <summary>席ごとの「人が立つ点」(席に入れるかの確かめに使う)</summary>
public List<PlanSeatSpot> spots = new List<PlanSeatSpot>();
}
[Serializable]
public class AisleInfo
{
public string name;
public bool narrow;
public Bounds zone;
public Vector3 entrance;
public float width;
}
[Header("設定(数字はすべてこの .asset の中)")]
public StoreLayout layout;
[Header("部品")]
public GameObject floorPrefab;
public GameObject wallPrefab;
public GameObject benchPrefab; // ベンチと仕切り(高さ 1.1 m のもの)
public GameObject benchSeatPrefab; // ベンチの座る場所の見た目(当たりなし。島の卓の椅子と同じ色)
public GameObject tablePrefab;
public GameObject chairPrefab;
public GameObject kitchenPassPrefab; // カウンター類
public GameObject aisleMarkPrefab; // 通路の色板(当たりなし)
[Header("床の塗り分け(見た目だけ)")]
[Tooltip("ロボットの体が入れる床の色(水色)")]
public Material robotFloorMaterial;
[Tooltip("人だけが入れる床の色(薄い黄色)")]
public Material peopleFloorMaterial;
[Header("組み立てた結果(読むだけ)")]
[SerializeField] List<TableInfo> _tables = new List<TableInfo>();
[SerializeField] List<AisleInfo> _aisles = new List<AisleInfo>();
[SerializeField] float _builtAisleWidth;
[SerializeField] Vector2 _outerSize;
readonly Dictionary<PlaceKind, Vector3> _points = new Dictionary<PlaceKind, Vector3>();
/// <summary>
/// 同じ種類に点がいくつもある場所(ロボットの待機場所・ドリンクバーの立つ点など)。
/// _points には最初の1つしか入らないので、別に全部を持つ。
/// </summary>
readonly Dictionary<PlaceKind, List<Vector3>> _pointLists =
new Dictionary<PlaceKind, List<Vector3>>();
readonly List<Transform> _notWalkableRoots = new List<Transform>();
StorePlan _plan;
/// <summary>
/// 上面を歩ける場所にしてはいけないものの親(壁・ベンチ・仕切り・卓・椅子・カウンター)。
/// NavMeshBaker がここを Not Walkable にして焼く。
/// </summary>
public IList<Transform> NotWalkableRoots { get { return _notWalkableRoots; } }
public IList<TableInfo> Tables { get { return _tables; } }
public IList<AisleInfo> Aisles { get { return _aisles; } }
public float BuiltAisleWidth { get { return _builtAisleWidth; } }
public Vector2 OuterSize { get { return _outerSize; } }
public StorePlan Plan { get { return _plan; } }
/// <summary>場所の点。無ければ原点</summary>
public Vector3 PointOf(PlaceKind kind)
{
Vector3 p;
if (_points.TryGetValue(kind, out p)) return p;
return Vector3.zero;
}
public bool HasPoint(PlaceKind kind) { return _points.ContainsKey(kind); }
/// <summary>その種類の場所の点をすべて返す(出てきた順)。無ければ空</summary>
public IList<Vector3> PointsOf(PlaceKind kind)
{
List<Vector3> list;
if (_pointLists.TryGetValue(kind, out list)) return list;
return new List<Vector3>();
}
// ロボットが使う点
public Vector3 PassPoint { get { return PointOf(PlaceKind.FoodPass); } }
public Vector3 ReturnPoint { get { return PointOf(PlaceKind.DishReturn); } }
public Vector3 PantryPoint { get { return PointOf(PlaceKind.Pantry); } }
/// <summary>ロボットの待機場所(B2)。受け渡し口の前とは別の点</summary>
public IList<Vector3> RobotStandbyPoints { get { return PointsOf(PlaceKind.RobotStandby); } }
// 人が使う点
public Vector3 RegisterPoint { get { return PointOf(PlaceKind.Register); } }
public Vector3 ToiletPoint { get { return PointOf(PlaceKind.Toilet); } }
public Vector3 DrinkBarPoint { get { return PointOf(PlaceKind.DrinkBar); } }
public Vector3 EntrancePoint { get { return PointOf(PlaceKind.Entrance); } }
public Vector3 WaitingPoint { get { return PointOf(PlaceKind.Waiting); } }
/// <summary>レジの前に並ぶ点(会計に立つ人が1人ずつ並ぶ)</summary>
public IList<Vector3> RegisterQueuePoints { get { return PointsOf(PlaceKind.RegisterQueue); } }
/// <summary>手の空いたスタッフが待つ点(1人に1つ)</summary>
public IList<Vector3> StaffStandbyPoints { get { return PointsOf(PlaceKind.StaffStandby); } }
/// <summary>レジの内側(店員側)の広がり。無ければ空</summary>
public PlanRect RegisterBackRect
{
get { return _plan != null ? _plan.RectOf(PlaceKind.RegisterBack) : new PlanRect(0f, 0f, 0f, 0f); }
}
/// <summary>レジの内側(スタッフが会計に立つ点)。無ければレジの前の点</summary>
public Vector3 RegisterBackPoint
{
get { return HasPoint(PlaceKind.RegisterBack) ? PointOf(PlaceKind.RegisterBack) : RegisterPoint; }
}
/// <summary>待合の広さ(待つ客を散らすのに使う)</summary>
public Bounds WaitingArea { get; private set; }
readonly List<Vector3> _refuges = new List<Vector3>();
/// <summary>人がロボットをよけて下がれる場所(通路の外)。壁ぞいの卓のすき間など</summary>
public IList<Vector3> Refuges { get { return _refuges; } }
/// <summary>床の塗り分けを入れる親(当たりなし。NavMesh には焼かれない)</summary>
public Transform FloorPaintRoot
{
get
{
Transform t = transform.Find("FloorPaint");
if (t == null)
{
GameObject go = new GameObject("FloorPaint");
go.transform.SetParent(transform, false);
t = go.transform;
}
return t;
}
}
/// <summary>
/// 床を塗り分け直す(見た目だけ)。
/// 水色=ロボットの体が入れる床、薄い黄色=人だけが入れる床。
/// NavMesh も当たりも変えない。
/// </summary>
public FloorPaint.Result PaintFloor(NavMeshBaker baker, float personRadius)
{
FloorPaint.Result res = new FloorPaint.Result();
if (layout == null || baker == null || _plan == null) return res;
Transform root = FloorPaintRoot;
if (!layout.showFloorPaint) { FloorPaint.Clear(root); return res; }
UnityEngine.AI.NavMeshTriangulation robotTri =
UnityEngine.AI.NavMesh.CalculateTriangulation();
UnityEngine.AI.NavMeshTriangulation peopleTri = baker.TriangulateFor(personRadius);
return FloorPaint.Rebuild(root, layout, _plan.NavBounds(layout.navBoundsPadding),
robotTri, peopleTri, personRadius, _plan,
robotFloorMaterial, peopleFloorMaterial);
}
/// <summary>
/// その点がある場所の名前(レジ・待合・ドリンクバーなど)。どこでもなければ空。
/// 記録で「どこで起きたか」を通路の名前だけでなく場所の名前でも言うために使う。
/// </summary>
public string PlaceNameAt(Vector3 p)
{
if (_plan == null) return "";
for (int i = 0; i < _plan.places.Count; i++)
{
PlanPlace pl = _plan.places[i];
if (pl.rect.IsEmpty) continue;
if (pl.rect.Contains(p)) return PlaceName(pl.kind);
}
return "";
}
/// <summary>場所の種類の名前(記録に出す)</summary>
public static string PlaceName(PlaceKind kind)
{
switch (kind)
{
case PlaceKind.Kitchen: return "厨房";
case PlaceKind.Pantry: return "パントリー";
case PlaceKind.FoodPass: return "受け渡し口";
case PlaceKind.DishReturn: return "返却口";
case PlaceKind.Entrance: return "入口";
case PlaceKind.Vestibule: return "風除室";
case PlaceKind.Waiting: return "待合";
case PlaceKind.Register: return "レジ";
case PlaceKind.Toilet: return "トイレ";
case PlaceKind.DrinkBar: return "ドリンクバー";
case PlaceKind.Office: return "事務所";
case PlaceKind.RobotStandby: return "ロボット待機";
case PlaceKind.DrinkBarStand: return "ドリンクバーの前";
case PlaceKind.DrinkBarQueue: return "ドリンクバーの列";
case PlaceKind.RegisterQueue: return "レジの列";
case PlaceKind.StaffStandby: return "スタッフ待機";
case PlaceKind.RegisterBack: return "レジの内側";
}
return "";
}
/// <summary>いまの layout.aisleWidthIndex で店を組み立て直す</summary>
public void Build()
{
if (layout == null)
{
Debug.LogError("[ServiceSim] StoreBuilder に StoreLayout が入っていません");
return;
}
Clear();
_plan = layout.NewPlan();
if (_plan == null || _plan.seats.Count == 0)
{
Debug.LogError("[ServiceSim] StoreLayout が部品の一覧を作れませんでした(席が0)");
return;
}
_builtAisleWidth = _plan.aisleWidth;
_outerSize = new Vector2(_plan.floor.SizeX, _plan.floor.SizeZ);
BuildFloor(_plan);
BuildBoxes(_plan);
BuildPlaces(_plan);
BuildSeats(_plan);
BuildAisles(_plan);
BuildAisleMarks(_plan);
_refuges.AddRange(_plan.refuges);
}
/// <summary>組み立てたものを消す</summary>
public void Clear()
{
_tables.Clear();
_aisles.Clear();
_points.Clear();
_pointLists.Clear();
_refuges.Clear();
_notWalkableRoots.Clear();
_plan = null;
WaitingArea = new Bounds();
for (int i = transform.childCount - 1; i >= 0; i--)
DestroyChild(transform.GetChild(i).gameObject);
}
/// <summary>細い通路の幅を選び直して組み立て直す</summary>
public void SetAisleWidthIndex(int index)
{
if (layout == null) return;
layout.aisleWidthIndex = index;
Build();
}
// ---------------- 床 ----------------
void BuildFloor(StorePlan plan)
{
Transform g = NewGroup("Floor", true);
if (plan.floorPieces.Count == 0)
{
PlaceFloor(g, plan, plan.floor, "Floor");
return;
}
for (int i = 0; i < plan.floorPieces.Count; i++)
PlaceFloor(g, plan, plan.floorPieces[i], "Floor" + (i + 1).ToString("00"));
}
void PlaceFloor(Transform parent, StorePlan plan, PlanRect r, string name)
{
GameObject floor = Box(floorPrefab, parent, name, r, plan.floorThickness);
if (floor != null)
floor.transform.position = new Vector3(r.CenterX, -plan.floorThickness * 0.5f, r.CenterZ);
}
// ---------------- 箱(壁・仕切り・ベンチ)----------------
void BuildBoxes(StorePlan plan)
{
Transform walls = NewGroup("Walls", false);
Transform seats = NewGroup("SeatStructure", false);
// 座る場所の見た目は、当たりを持たない prefab でしか置かない。
// 当たりのある prefab で代わりに置くと NavMesh が変わってしまうため、
// prefab が無いときは置かずに知らせる(Setup を実行すれば作られる)。
bool seatLook = benchSeatPrefab != null;
int skippedSeatLooks = 0;
for (int i = 0; i < plan.boxes.Count; i++)
{
PlanBox b = plan.boxes[i];
if (b.style == BoxStyle.BenchSeat && !seatLook) { skippedSeatLooks++; continue; }
Transform parent = (b.style == BoxStyle.Bench || b.style == BoxStyle.Divider ||
b.style == BoxStyle.BenchSeat) ? seats : walls;
GameObject go = Box(PrefabFor(b.style), parent, b.name, b.rect, b.height);
if (go != null && b.baseY != 0f)
go.transform.position += Vector3.up * b.baseY;
}
if (skippedSeatLooks > 0)
Debug.LogWarning("[ServiceSim] ベンチの座る場所の見た目 " + skippedSeatLooks +
" 個を置きませんでした(BenchSeat.prefab がありません)。" +
"ServiceSim/Setup を実行してください");
}
// ---------------- 場所 ----------------
void BuildPlaces(StorePlan plan)
{
Transform g = NewGroup("Places", false);
for (int i = 0; i < plan.places.Count; i++)
{
PlanPlace p = plan.places[i];
if (p.solid && p.height > 0.0005f)
Box(PrefabFor(p.kind), g, p.name, p.rect, p.height);
NewChild(g, p.name + "_Point").position = p.point;
if (!_points.ContainsKey(p.kind)) _points.Add(p.kind, p.point);
List<Vector3> list;
if (!_pointLists.TryGetValue(p.kind, out list))
{
list = new List<Vector3>();
_pointLists.Add(p.kind, list);
}
list.Add(p.point);
if (p.kind == PlaceKind.Waiting) WaitingArea = p.rect.ToBounds(0f, 2f);
}
}
// ---------------- 席 ----------------
void BuildSeats(StorePlan plan)
{
Transform g = NewGroup("Seats", false);
for (int i = 0; i < plan.seats.Count; i++)
{
PlanSeat s = plan.seats[i];
Transform unit = NewChild(g, "Table" + s.tableId.ToString("00"));
Box(tablePrefab, unit, "TableTop", s.tableRect, s.tableHeight);
TableInfo info = new TableInfo();
info.tableId = s.tableId;
info.kind = s.kind;
info.root = unit;
info.center = s.tableRect.Center;
info.deliveryPoint = s.deliveryPoint;
info.aisleIndex = s.aisleIndex;
info.seatCount = s.seatCount;
info.spots.AddRange(s.spots);
PlanAisle aisle = plan.AisleAt(s.aisleIndex);
if (aisle != null)
{
info.aisleZone = aisle.Zone(-1f, 2f);
info.aisleEntrance = aisle.entrance;
}
else
{
info.aisleEntrance = s.deliveryPoint;
}
for (int c = 0; c < s.chairs.Count; c++)
{
PlanChair pc = s.chairs[c];
GameObject go = Box(chairPrefab, unit, "Chair", pc.rect, pc.height);
if (go == null) continue;
Chair chair = go.GetComponent<Chair>();
if (chair == null) continue;
chair.isAisleSide = pc.pullable;
chair.pullDirection = pc.pullDirection;
chair.MarkParked();
if (pc.pullable) info.aisleChairs.Add(chair);
}
_tables.Add(info);
}
}
// ---------------- 通路 ----------------
void BuildAisles(StorePlan plan)
{
for (int i = 0; i < plan.aisles.Count; i++)
{
PlanAisle a = plan.aisles[i];
AisleInfo info = new AisleInfo();
info.name = a.name;
info.narrow = a.narrow;
info.zone = a.Zone(-1f, 2f);
info.entrance = a.entrance;
info.width = Mathf.Min(a.rect.SizeX, a.rect.SizeZ);
_aisles.Add(info);
}
}
/// <summary>
/// 通路の床に色の板を敷く。当たりは持たないので NavMesh には焼かれず、計測には効かない。
/// </summary>
void BuildAisleMarks(StorePlan plan)
{
if (!layout.showAisleMarks || aisleMarkPrefab == null) return;
Transform g = NewGroup("AisleMarks", false);
for (int i = 0; i < plan.aisles.Count; i++)
{
PlanAisle a = plan.aisles[i];
Box(aisleMarkPrefab, g, "Mark_" + a.name, a.rect, layout.aisleMarkThickness);
}
}
// ---------------- 小さな道具 ----------------
GameObject PrefabFor(BoxStyle style)
{
switch (style)
{
case BoxStyle.Floor: return floorPrefab;
case BoxStyle.Table: return tablePrefab;
case BoxStyle.Chair: return chairPrefab;
case BoxStyle.Bench: return benchPrefab != null ? benchPrefab : wallPrefab;
case BoxStyle.Divider: return benchPrefab != null ? benchPrefab : wallPrefab;
case BoxStyle.BenchSeat: return benchSeatPrefab; // 無いときは BuildBoxes が置かない
case BoxStyle.Counter: return kitchenPassPrefab;
case BoxStyle.AisleMark: return aisleMarkPrefab;
default: return wallPrefab;
}
}
GameObject PrefabFor(PlaceKind kind)
{
switch (kind)
{
case PlaceKind.Kitchen:
case PlaceKind.Office:
return wallPrefab;
default:
return kitchenPassPrefab;
}
}
/// <summary>
/// 組み立てたものを入れる親を作る。
/// walkable が false の親は、NavMesh で Not Walkable として焼く(上面に乗れないように)。
/// </summary>
Transform NewGroup(string name, bool walkable)
{
Transform t = NewChild(transform, name);
if (!walkable) _notWalkableRoots.Add(t);
return t;
}
Transform NewChild(Transform parent, string name)
{
GameObject go = new GameObject(name);
go.transform.SetParent(parent, false);
return go.transform;
}
/// <summary>長方形と高さから箱を1つ置く(下の面が y=0、上の面が y=height)</summary>
GameObject Box(GameObject prefab, Transform parent, string name, PlanRect rect, float height)
{
if (rect.IsEmpty || height <= 0.0005f) return null;
GameObject go;
if (prefab != null)
{
go = Instantiate(prefab, parent);
}
else
{
go = GameObject.CreatePrimitive(PrimitiveType.Cube);
go.transform.SetParent(parent, false);
}
go.name = name;
go.transform.localScale = new Vector3(rect.SizeX, height, rect.SizeZ);
go.transform.position = new Vector3(rect.CenterX, height * 0.5f, rect.CenterZ);
return go;
}
static void DestroyChild(GameObject go)
{
if (Application.isPlaying) Destroy(go);
else DestroyImmediate(go);
}
}
}
Store/StoreLayout.cs
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 店の設定ファイルの共通部分。
/// 店の形そのものは、種類ごとの実装(GridStoreLayout / BandStoreLayout)が
/// StorePlan(部品をどこに置くかの一覧)として作る。
///
/// 新しい店の形は、StoreLayout の .asset を1つ足すだけで作れる(コードは触らない)。
/// 店に関する数字はすべて .asset に置く。
/// </summary>
public abstract class StoreLayout : ScriptableObject
{
[Header("細い通路の幅")]
[Tooltip("細い通路の幅の選択肢(m)。止まっている椅子の背と、隣の席の背または壁のあいだで測る")]
public float[] aisleWidthOptions = { 0.8f, 1.0f, 1.2f };
[Tooltip("いま使う細い通路の幅(aisleWidthOptions の番号)")]
public int aisleWidthIndex = 2;
[Header("ロボットの止まる点")]
[Tooltip("卓に料理を置く停止点を、細い通路の中心から卓の側へずらす距離(m)")]
public float deliveryOffsetFromAisleCenter = 0f;
[Tooltip("細い通路に入る前に待つ位置を、通路の入口から手前へ下げる距離(m)")]
public float aisleEntranceSetback = 0.8f;
[Tooltip("入口で待つ位置を、通路の中心から横へずらす距離(m)。待っている台の横を、戻る台が通れるようにする")]
public float aisleEntranceSideOffset = 0.45f;
[Header("床と壁")]
public float wallHeight = 2.4f;
public float wallThickness = 0.15f;
[Tooltip("床の厚み(m)")]
public float floorThickness = 0.2f;
[Header("見分けやすさ(計測には効かない)")]
[Tooltip("通路の床に色の板を敷くか。当たりは持たないので NavMesh には焼かれない")]
public bool showAisleMarks = true;
[Tooltip("通路の色板の厚み(m)")]
public float aisleMarkThickness = 0.02f;
[Header("床の塗り分け(見た目だけ。NavMesh は変えない)")]
[Tooltip("床の塗り分けを出すか。水色=ロボットの体が入れる床、薄い黄色=人だけが入れる床")]
public bool showFloorPaint = true;
[Tooltip("塗り分けを作るときのマスの大きさ(m)。小さいほどきれいだが重い")]
public float floorPaintCellSize = 0.05f;
[Tooltip("塗りの板を置く高さ(m)。通路の色板より上にして、塗りが見えるようにする")]
public float floorPaintHeight = 0.03f;
[Tooltip("場所の名前(レジ・待合・ドリンクバーなど)を床の上に出すか。計測の回では出さない")]
public bool showPlaceNames = true;
[Tooltip("場所の名前の文字の大きさ(m)")]
public float placeNameSize = 0.14f;
[Tooltip("場所の名前を置く高さ(m)。床の塗りより上にする")]
public float placeNameHeight = 0.06f;
[Header("NavMesh のベイク設定(自前ベイカーが使う)")]
[Tooltip("経路を焼くときの通行者の半径(m)。ロボットの半径に合わせる")]
public float navAgentRadius = 0.25f;
public float navAgentHeight = 1.2f;
public float navAgentClimb = 0.2f;
public float navAgentSlope = 10f;
[Tooltip("ベイクの細かさ(m)。0.8 m の通路を正しく出すために細かくする")]
public float navVoxelSize = 0.05f;
[Tooltip("これより小さい島は捨てる(m^2)")]
public float navMinRegionArea = 0.3f;
[Tooltip("ベイクする範囲に足す余白(m)")]
public float navBoundsPadding = 1.0f;
// ---- 共通で使えるもの ----
/// <summary>いま使う細い通路の幅(m)</summary>
public float AisleWidth
{
get
{
if (aisleWidthOptions == null || aisleWidthOptions.Length == 0) return 1.0f;
int i = Mathf.Clamp(aisleWidthIndex, 0, aisleWidthOptions.Length - 1);
return aisleWidthOptions[i];
}
}
/// <summary>細い通路の幅のうち、いちばん狭いもの(m)</summary>
public float MinAisleWidth
{
get
{
float m = float.MaxValue;
if (aisleWidthOptions != null)
for (int i = 0; i < aisleWidthOptions.Length; i++)
if (aisleWidthOptions[i] > 0f) m = Mathf.Min(m, aisleWidthOptions[i]);
return m < float.MaxValue ? m : AisleWidth;
}
}
/// <summary>細い通路の幅のうち、いちばん広いもの(m)</summary>
public float MaxAisleWidth
{
get
{
float m = 0f;
if (aisleWidthOptions != null)
for (int i = 0; i < aisleWidthOptions.Length; i++) m = Mathf.Max(m, aisleWidthOptions[i]);
return m > 0f ? m : 1.0f;
}
}
public int AisleWidthCount
{
get { return aisleWidthOptions != null ? aisleWidthOptions.Length : 0; }
}
/// <summary>
/// 卓の停止点を通路の中心からずらす距離。
/// 通路が狭いときは、NavMesh の上に乗る範囲までに抑える。
/// </summary>
public float DeliveryOffset()
{
float limit = Mathf.Max(0f, AisleWidth * 0.5f - navAgentRadius - 0.02f);
return Mathf.Min(deliveryOffsetFromAisleCenter, limit);
}
/// <summary>この .asset の形を、部品の一覧(StorePlan)にする</summary>
protected abstract StorePlan Compose();
/// <summary>店の種類の名前(報告に出す)</summary>
public virtual string StoreKindName { get { return GetType().Name; } }
/// <summary>
/// 入口まわり(ドリンクバー・待合・レジ・入口)が帯に入ったかの報告。
/// Build Store が store_report.txt と Console に出す。入らなかったものはここに出す。
/// </summary>
public virtual string FrontFitReport() { return ""; }
/// <summary>
/// ロボットの待機場所(B2)の報告。点の座標・点どうしの距離・
/// 太い通路に残る通り道の幅・スタッフが立つ点との距離を出す。
/// Build Store が store_report.txt と Console に出す。
/// </summary>
public virtual string RobotStandbyReport() { return ""; }
/// <summary>いまの幅で部品の一覧を作る</summary>
public StorePlan NewPlan()
{
StorePlan plan = Compose();
if (plan == null) return null;
plan.wallHeight = wallHeight;
plan.wallThickness = wallThickness;
plan.floorThickness = floorThickness;
plan.aisleWidth = AisleWidth;
return plan;
}
}
}
Store/StoreLayout_Family.asset
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6f4fb03fd01dc4444ba2b4e5f289aee3, type: 3}
m_Name: StoreLayout_Family
m_EditorClassIdentifier:
aisleWidthOptions:
- 0.8
- 1
- 1.2
aisleWidthIndex: 1
deliveryOffsetFromAisleCenter: 0
aisleEntranceSetback: 0.8
aisleEntranceSideOffset: 0.45
wallHeight: 2.4
wallThickness: 0.15
floorThickness: 0.2
showAisleMarks: 1
aisleMarkThickness: 0.02
showFloorPaint: 1
floorPaintCellSize: 0.05
floorPaintHeight: 0.03
showPlaceNames: 1
placeNameSize: 0.14
placeNameHeight: 0.06
navAgentRadius: 0.25
navAgentHeight: 1.2
navAgentClimb: 0.2
navAgentSlope: 10
navVoxelSize: 0.05
navMinRegionArea: 0.3
navBoundsPadding: 1
mainAisleWidth: 1.6
crossAisleWidth: 1.2
kitchenDepthX: 10
serviceStripDepth: 0.8
toiletSize: {x: 3, y: 4}
toiletDoorWidthZ: 1
passFromNorthZ: 1.5
passWindowLengthZ: 1.2
pantryFromNorthZ: 2.5
pantryLengthZ: 4.5
returnFromNorthZ: 8
returnWindowLengthZ: 1
wallBoothCount: 8
boothTableSize: {x: 1.4, y: 0.75}
benchDepth: 0.55
benchSeatHeight: 0.45
benchBackThickness: 0.1
benchHeight: 1.1
benchSeatGapX: 0.05
benchSeatPadThickness: 0.02
wallTableSize: {x: 0.7, y: 0.6}
wallTableGapX: 0.6
wallTableCountFromNarrowestAisle: 1
seatApproachGapZ: 0.3
islandColumns: 3
islandTablesPerColumn: 3
islandTableSize: {x: 0.75, y: 1.2}
islandTableGapZ: 0.1
chairDepth: 0.45
chairWidth: 0.45
chairHeight: 0.45
tableHeight: 0.72
dividerHeight: 1.1
entranceWidthX: 2
vestibuleDepthZ: 1.2
waitingDepthZ: 0
registerSize: {x: 0.8, y: 0.5}
registerQueueCount: 3
registerQueuePitch: 0.5
registerBackDepthZ: 1.2
registerBackMinOpenZ: 0.7
drinkBarSize: {x: 3.6, y: 0.7}
counterHeight: 1
approachGap: 0.6
waitingStandCount: 8
waitingStandAreaM2: 0.5
robotStandbyCount: 2
robotStandbySideOffset: 0.4
robotStandbyFromPassZ: 1.6
robotStandbyPitchZ: 0.8
robotStandbyMinClearX: 0.55
staffStandbyCount: 2
staffStandbyFromPassZ: 0.45
staffStandbyPitchX: 0.6
staffStandbySideOffset: 0.4
drinkBarStandCount: 4
drinkBarStandDepthZ: 1.5
drinkBarQueueCount: 3
drinkBarQueuePitch: 0.5
drinkBarQueueDepthZ: 0.6
drinkBarAisleClearZ: 0.1
Store/StoreLayout_Grid.asset
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a69ee8612d7ba42369abb20536652cc9, type: 3}
m_Name: StoreLayout_Grid
m_EditorClassIdentifier:
aisleWidthOptions:
- 0.8
- 1
- 1.2
aisleWidthIndex: 2
deliveryOffsetFromAisleCenter: 0.15
aisleEntranceSetback: 0.8
aisleEntranceSideOffset: 0.45
wallHeight: 2.4
wallThickness: 0.15
floorThickness: 0.2
showAisleMarks: 1
aisleMarkThickness: 0.02
showFloorPaint: 1
floorPaintCellSize: 0.05
floorPaintHeight: 0.03
showPlaceNames: 1
placeNameSize: 0.14
placeNameHeight: 0.06
navAgentRadius: 0.25
navAgentHeight: 1.2
navAgentClimb: 0.2
navAgentSlope: 10
navVoxelSize: 0.05
navMinRegionArea: 0.3
navBoundsPadding: 1
tableCount: 35
branchAisleCenterZ:
- 4.9
- 9.3
- 13.7
unitsPerBranchSide: 6
unitPitchX: 1.4
firstUnitCenterX: 1.35
drinkBarSlotIndex: 35
mainAisleWidth: 1.5
rearCrossAisle: 0
tableSize: {x: 1.2, y: 0.72, z: 0.8}
chairSize: {x: 0.45, y: 0.45, z: 0.4}
chairOffsetX: 0.3
boothDividerHeight: 1.2
passCounterSize: {x: 1.6, y: 1, z: 0.5}
passPoint: {x: 0, y: 0, z: 0.15}
registerCounterSize: {x: 2, y: 1, z: 0.6}
registerCenterX: 2.4
toiletBlockSize: {x: 2.4, y: 2.4, z: 1.6}
toiletCenterX: 7.6
poiApproachGap: 0.7
drinkBarHeight: 1.1
frontPocketDepth: 3.4
Store/StorePlan.cs
using System.Collections.Generic;
using UnityEngine;
namespace ServiceSim
{
/// <summary>席の種類</summary>
public enum SeatKind
{
WallBooth, // 壁際ブース(両側ベンチ)
WallTable, // 壁沿いの卓(壁側ベンチ+通路側に椅子)
IslandTable, // 島の卓(両側に椅子)
Counter, // カウンター席(今回は使わないが置ける)
LargeTable // 大人数席(今回は使わないが置ける)
}
/// <summary>場所の種類</summary>
public enum PlaceKind
{
Kitchen, // 厨房(通れない)
Pantry, // パントリー(水・おしぼり・カトラリー)
FoodPass, // 料理の受け渡し口
DishReturn, // 下げ膳の返却口
Entrance, // 入口
Vestibule, // 風除室
Waiting, // 待合
Register, // レジ
Toilet, // トイレ
DrinkBar, // ドリンクバー
Office, // 事務所・更衣室
RobotStandby, // ロボットの待機場所(受け渡し口の前をふさがないため)B2
DrinkBarStand,// ドリンクバーの前に立つ点(機械1台に1つ)B4
DrinkBarQueue,// ドリンクバーの並ぶ点 B4
RegisterQueue,// レジの前に並ぶ点(会計に立つ人が1人ずつ並ぶ)
StaffStandby, // 手の空いたスタッフが待つ点(1人に1つ。太い通路の通り道をふさがない)
RegisterBack // レジの内側(スタッフだけが入れる。太い通路からだけ入れる)
}
/// <summary>箱の見た目(StoreBuilder がどの prefab を使うかを決める)</summary>
public enum BoxStyle
{
Floor, Wall, Divider, Table, Bench, Chair, Counter, AisleMark,
/// <summary>
/// ベンチの座る場所の見た目(1本につき2つ)。当たりを持たないので
/// NavMesh にも、前のセンサーにも、左右の空きの測りにも効かない。見た目だけ。
/// </summary>
BenchSeat
}
/// <summary>XZ 平面の長方形(y は持たない)</summary>
public struct PlanRect
{
public float xMin, xMax, zMin, zMax;
public PlanRect(float xMin, float zMin, float xMax, float zMax)
{
this.xMin = Mathf.Min(xMin, xMax);
this.xMax = Mathf.Max(xMin, xMax);
this.zMin = Mathf.Min(zMin, zMax);
this.zMax = Mathf.Max(zMin, zMax);
}
public static PlanRect FromSize(float xMin, float zMin, float sizeX, float sizeZ)
{
return new PlanRect(xMin, zMin, xMin + sizeX, zMin + sizeZ);
}
public static PlanRect FromCenter(Vector3 center, float sizeX, float sizeZ)
{
return new PlanRect(center.x - sizeX * 0.5f, center.z - sizeZ * 0.5f,
center.x + sizeX * 0.5f, center.z + sizeZ * 0.5f);
}
public float SizeX { get { return xMax - xMin; } }
public float SizeZ { get { return zMax - zMin; } }
public float CenterX { get { return (xMin + xMax) * 0.5f; } }
public float CenterZ { get { return (zMin + zMax) * 0.5f; } }
public Vector3 Center { get { return new Vector3(CenterX, 0f, CenterZ); } }
public bool IsEmpty { get { return SizeX <= 0.0005f || SizeZ <= 0.0005f; } }
public bool Contains(Vector3 p)
{
return p.x >= xMin && p.x <= xMax && p.z >= zMin && p.z <= zMax;
}
public PlanRect Expand(float m)
{
return new PlanRect(xMin - m, zMin - m, xMax + m, zMax + m);
}
public Bounds ToBounds(float yMin, float yMax)
{
Bounds b = new Bounds();
b.SetMinMax(new Vector3(xMin, yMin, zMin), new Vector3(xMax, yMax, zMax));
return b;
}
public override string ToString()
{
return "x " + xMin.ToString("0.##") + ".." + xMax.ToString("0.##") +
" / z " + zMin.ToString("0.##") + ".." + zMax.ToString("0.##");
}
}
/// <summary>置く箱1つ</summary>
public class PlanBox
{
public string name;
public BoxStyle style;
public PlanRect rect;
public float height;
/// <summary>下の面の高さ(m)。ふつうは 0</summary>
public float baseY;
public PlanBox(string name, BoxStyle style, PlanRect rect, float height)
{
this.name = name;
this.style = style;
this.rect = rect;
this.height = height;
this.baseY = 0f;
}
}
/// <summary>椅子1脚(引けるものと引けないもの)</summary>
public class PlanChair
{
public PlanRect rect;
public float height;
/// <summary>引けるか。ブースのベンチは false</summary>
public bool pullable;
/// <summary>引く向き(通路の側へ)。XZ の単位ベクトル</summary>
public Vector3 pullDirection;
}
/// <summary>
/// 座る場所1つぶんの「人が立つ点」。
/// 通路からここまで人が歩けるかを Build Store が確かめる(席に入れない卓を見つけるため)。
/// </summary>
public struct PlanSeatSpot
{
/// <summary>どの席か(報告に出す。例「壁側」「通路側」「南ベンチ」)</summary>
public string name;
/// <summary>その席に着くために人が立つ点</summary>
public Vector3 point;
public PlanSeatSpot(string name, Vector3 point)
{
this.name = name;
this.point = point;
}
}
/// <summary>卓1つ</summary>
public class PlanSeat
{
public int tableId;
public SeatKind kind;
public PlanRect tableRect;
public float tableHeight;
/// <summary>何人かけの卓か(2人席・4人席)。組の座らせ方に使う</summary>
public int seatCount = 4;
/// <summary>ロボットが料理を置くために止まる点(通路の上)</summary>
public Vector3 deliveryPoint;
/// <summary>この卓に行くときに使う細い通路の番号(譲り合いの区画に使う)</summary>
public int aisleIndex = -1;
public readonly List<PlanChair> chairs = new List<PlanChair>();
/// <summary>席ごとの「人が立つ点」(Build Store の席への入りやすさの確かめに使う)</summary>
public readonly List<PlanSeatSpot> spots = new List<PlanSeatSpot>();
}
/// <summary>通路1本(譲り合いの区画と、入る前に待つ点)</summary>
public class PlanAisle
{
public string name;
public PlanRect rect;
/// <summary>細い通路か(幅を切り替える対象か)</summary>
public bool narrow;
/// <summary>この通路に入る前に待つ点</summary>
public Vector3 entrance;
public Bounds Zone(float yMin, float yMax) { return rect.ToBounds(yMin, yMax); }
}
/// <summary>場所1つ(厨房・パントリー・受け渡し口など)</summary>
public class PlanPlace
{
public PlaceKind kind;
public string name;
public PlanRect rect;
public float height;
/// <summary>人が立つ・ロボットが止まる点</summary>
public Vector3 point;
/// <summary>通れない箱として置くか</summary>
public bool solid;
}
/// <summary>
/// 店の「部品をどこに置くか」の一覧。
/// StoreLayout(の種類ごとの実装)がこれを作り、StoreBuilder がこれだけを見て並べる。
/// StoreBuilder は格子や帯の組み方を知らない。
/// </summary>
public class StorePlan
{
/// <summary>床の全体の広がり(カメラの画角とベイク範囲に使う)</summary>
public PlanRect floor;
/// <summary>
/// 実際に床を敷く長方形。建物の外に床が出ないように分けて持つ。
/// 空なら floor を1枚だけ敷く。
/// </summary>
public readonly List<PlanRect> floorPieces = new List<PlanRect>();
public float wallHeight = 2.4f;
public float wallThickness = 0.15f;
public float floorThickness = 0.2f;
/// <summary>この plan を作ったときの細い通路の幅(m)</summary>
public float aisleWidth;
/// <summary>
/// 人がロボットをよけるときに下がれる場所(通路の外)。
/// 壁ぞいの卓と卓のすき間など。店の形が入れる。
/// </summary>
public readonly List<Vector3> refuges = new List<Vector3>();
public readonly List<PlanBox> boxes = new List<PlanBox>();
public readonly List<PlanSeat> seats = new List<PlanSeat>();
public readonly List<PlanAisle> aisles = new List<PlanAisle>();
public readonly List<PlanPlace> places = new List<PlanPlace>();
// ---- 組み立てを助ける小さな道具 ----
/// <summary>床を1枚足す。floor の広がりも合わせて広げる</summary>
public void AddFloor(PlanRect rect)
{
if (rect.IsEmpty) return;
if (floorPieces.Count == 0) floor = rect;
else
{
floor = new PlanRect(
Mathf.Min(floor.xMin, rect.xMin), Mathf.Min(floor.zMin, rect.zMin),
Mathf.Max(floor.xMax, rect.xMax), Mathf.Max(floor.zMax, rect.zMax));
}
floorPieces.Add(rect);
}
public PlanBox AddBox(string name, BoxStyle style, PlanRect rect, float height)
{
if (rect.IsEmpty || height <= 0.0005f) return null;
PlanBox b = new PlanBox(name, style, rect, height);
boxes.Add(b);
return b;
}
public PlanAisle AddAisle(string name, PlanRect rect, bool narrow, Vector3 entrance)
{
PlanAisle a = new PlanAisle();
a.name = name;
a.rect = rect;
a.narrow = narrow;
a.entrance = entrance;
aisles.Add(a);
return a;
}
public PlanPlace AddPlace(PlaceKind kind, string name, PlanRect rect, float height, Vector3 point, bool solid)
{
PlanPlace p = new PlanPlace();
p.kind = kind;
p.name = name;
p.rect = rect;
p.height = height;
p.point = point;
p.solid = solid;
places.Add(p);
return p;
}
/// <summary>場所の点を引く。無ければ fallback</summary>
public Vector3 PointOf(PlaceKind kind, Vector3 fallback)
{
for (int i = 0; i < places.Count; i++)
if (places[i].kind == kind) return places[i].point;
return fallback;
}
/// <summary>
/// 同じ種類の場所の点をすべて集める(出てきた順)。
/// ロボットの待機場所やドリンクバーの立つ点のように、1種類に点がいくつもあるときに使う。
/// </summary>
public List<Vector3> PointsOf(PlaceKind kind)
{
List<Vector3> list = new List<Vector3>();
for (int i = 0; i < places.Count; i++)
if (places[i].kind == kind) list.Add(places[i].point);
return list;
}
public bool HasPlace(PlaceKind kind)
{
for (int i = 0; i < places.Count; i++)
if (places[i].kind == kind) return true;
return false;
}
public PlanRect RectOf(PlaceKind kind)
{
for (int i = 0; i < places.Count; i++)
if (places[i].kind == kind) return places[i].rect;
return new PlanRect(0f, 0f, 0f, 0f);
}
public PlanAisle AisleAt(int index)
{
if (index < 0 || index >= aisles.Count) return null;
return aisles[index];
}
/// <summary>細い通路の数</summary>
public int NarrowAisleCount
{
get
{
int n = 0;
for (int i = 0; i < aisles.Count; i++) if (aisles[i].narrow) n++;
return n;
}
}
public Bounds NavBounds(float padding)
{
PlanRect r = floor.Expand(padding);
return r.ToBounds(-1f, wallHeight + 1f);
}
}
}
Robot/FrontSensor.cs
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// ロボットの前に人・ロボット・椅子があるかを見る。
/// 見るだけで、止まる・動き出すの判断はしない(それは ServingRobotController)。
/// 止まった原因を分けられるように、いちばん近いものの種類も返す。
/// </summary>
public class FrontSensor : MonoBehaviour
{
public struct Reading
{
/// <summary>前に何かあるか</summary>
public bool blocked;
/// <summary>いちばん近いものまでの、ロボットの外周からの距離(m)。何もなければ sensorRange</summary>
public float nearestDistance;
public bool sawRobot;
public bool sawPerson;
public bool sawChair;
/// <summary>いちばん近いものが人か</summary>
public bool nearestIsPerson;
/// <summary>いちばん近いものが人のときの種類</summary>
public PersonKind nearestPersonKind;
/// <summary>いちばん近いものが人のときの速さ(m/秒)</summary>
public float nearestPersonSpeed;
/// <summary>いちばん近いものが人で、立ち止まっているか</summary>
public bool nearestPersonStanding;
/// <summary>いちばん近いものがロボットか</summary>
public bool nearestIsRobot;
/// <summary>いちばん近いものが椅子か</summary>
public bool nearestIsChair;
/// <summary>いちばん近いものの名前(cause_detail に使う)</summary>
public string nearestName;
}
[Header("設定")]
public RobotSpec spec;
[Tooltip("自分のロボット。自分を見ないようにするために使う")]
public ServingRobotController owner;
const int MaxHits = 32;
readonly Collider[] _hits = new Collider[MaxHits];
Reading _last;
public Reading Last { get { return _last; } }
/// <summary>いまの前の様子を見る</summary>
public Reading Sense()
{
Reading r = new Reading();
if (spec == null)
{
_last = r;
return r;
}
r.nearestDistance = spec.sensorRange;
r.nearestName = "";
float radius = spec.Radius;
Vector3 fwd = transform.forward;
Vector3 eye = transform.position + Vector3.up * (spec.sensorHeight * 0.5f);
Vector3 center = eye + fwd * (radius + spec.sensorRange * 0.5f);
Vector3 half = new Vector3(spec.sensorWidth * 0.5f, spec.sensorHeight * 0.5f, spec.sensorRange * 0.5f);
int n = Physics.OverlapBoxNonAlloc(center, half, _hits, transform.rotation, ~0, QueryTriggerInteraction.Ignore);
if (n > MaxHits) n = MaxHits;
for (int i = 0; i < n; i++)
{
Collider col = _hits[i];
if (col == null) continue;
PersonBody person = col.GetComponentInParent<PersonBody>();
ServingRobotController other = null;
Chair chair = null;
if (person == null)
{
other = col.GetComponentInParent<ServingRobotController>();
if (other != null && owner != null && other == owner) continue; // 自分は見ない
if (other == null) chair = col.GetComponentInParent<Chair>();
}
if (person == null && other == null && chair == null) continue; // 床・壁・卓は見ない
r.blocked = true;
if (person != null) r.sawPerson = true;
if (other != null) r.sawRobot = true;
if (chair != null) r.sawChair = true;
// ロボットの外周からの距離
Vector3 closest = col.ClosestPoint(eye);
Vector3 flat = closest - transform.position;
flat.y = 0f;
float d = Mathf.Max(0f, Vector3.Dot(flat, fwd) - radius);
if (d >= r.nearestDistance) continue;
r.nearestDistance = d;
r.nearestIsPerson = person != null;
r.nearestIsRobot = other != null;
r.nearestIsChair = chair != null;
if (person != null)
{
r.nearestPersonKind = person.kind;
r.nearestPersonSpeed = person.Speed;
r.nearestPersonStanding = person.IsStanding;
r.nearestName = person.name;
}
else if (other != null) r.nearestName = other.name;
else if (chair != null) r.nearestName = chair.transform.parent != null
? chair.transform.parent.name + "/" + chair.name : chair.name;
}
_last = r;
return r;
}
void OnDrawGizmosSelected()
{
if (spec == null) return;
Vector3 fwd = transform.forward;
Vector3 center = transform.position
+ Vector3.up * (spec.sensorHeight * 0.5f)
+ fwd * (spec.Radius + spec.sensorRange * 0.5f);
Gizmos.color = _last.blocked ? new Color(1f, 0.3f, 0.2f, 0.6f) : new Color(0.3f, 1f, 0.4f, 0.4f);
Matrix4x4 old = Gizmos.matrix;
Gizmos.matrix = Matrix4x4.TRS(center, transform.rotation, Vector3.one);
Gizmos.DrawWireCube(Vector3.zero, new Vector3(spec.sensorWidth, spec.sensorHeight, spec.sensorRange));
Gizmos.matrix = old;
}
}
}
Robot/RobotSpec.asset
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6b50de1a9617a4ad5b687f834c80800b, type: 3}
m_Name: RobotSpec
m_EditorClassIdentifier:
diameter: 0.5
height: 1.2
maxSpeed: 0.6
acceleration: 1
deceleration: 1
turnSpeedDegPerSec: 180
sensorRange: 0.6
sensorWidth: 0.5
sensorHeight: 1.2
resumeAfterClearSec: 1
replanAfterStoppedSec: 10
stuckAfterStoppedSec: 60
stuckRecoveryWaitSec: 15
maxTripSec: 600
aisleYielding: 1
tableDwellSec: 25
arriveRadius: 0.25
cornerReachDistance: 0.12
stoppedSpeedEpsilon: 0.02
clearanceProbeHeight: 0.3
clearanceProbeRange: 3
pathBlockSearchRadius: 2
avoidancePriority: 0
repathRetrySec: 1
noPathForceReplanSec: 10
escapeToAisleEntranceOnPartialPath: 1
Robot/RobotSpec.cs
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 配膳ロボットの数字。ロボットに関する数字はすべてこの .asset に置く。
/// </summary>
[CreateAssetMenu(menuName = "ServiceSim/Robot Spec", fileName = "RobotSpec")]
public class RobotSpec : ScriptableObject
{
[Header("大きさ")]
[Tooltip("直径(m)")]
public float diameter = 0.5f;
[Tooltip("高さ(m)")]
public float height = 1.2f;
[Header("走り")]
[Tooltip("最高速度(m/秒)")]
public float maxSpeed = 0.6f;
[Tooltip("加速(m/秒^2)")]
public float acceleration = 1.0f;
[Tooltip("減速(m/秒^2)")]
public float deceleration = 1.0f;
[Tooltip("向きを変える速さ(度/秒)。仮の値には無かったので追加した")]
public float turnSpeedDegPerSec = 180f;
[Header("前のセンサー")]
[Tooltip("進む向きの前、どこまで見るか(m)")]
public float sensorRange = 0.6f;
[Tooltip("センサーの幅(m)。ロボットの直径に合わせる")]
public float sensorWidth = 0.5f;
[Tooltip("センサーの高さ(m)")]
public float sensorHeight = 1.2f;
[Header("止まる・動き出す")]
[Tooltip("前がこの秒数だけ空いたら動き出す(秒)")]
public float resumeAfterClearSec = 1.0f;
[Tooltip("止まってこの秒数たったら経路を引き直す(秒)")]
public float replanAfterStoppedSec = 10f;
[Tooltip("この秒数動けなかったら「動けなくなった」と記録する(秒)")]
public float stuckAfterStoppedSec = 60f;
[Tooltip("動けなくなったあと、人が動かしに来るまでの待ち(秒)。この後、受け渡し口へ移して往復を終える")]
public float stuckRecoveryWaitSec = 15f;
[Tooltip("1往復がこの秒数を超えたら、受け渡し口へ置き直して往復を打ち切る(秒)。無限に止まらないための保険")]
public float maxTripSec = 600f;
[Header("通路の譲り合い")]
[Tooltip("行き先の細い通路にもう1台のロボットがいたら、太い通路側の入口で待つ。場面2の撮影では切る")]
public bool aisleYielding = true;
[Header("卓での止まり")]
[Tooltip("卓に着いたら止まる秒数(秒)。客が料理を取る時間。「止まった」には数えない")]
public float tableDwellSec = 25f;
[Header("経路をたどる(仮の値には無かったので追加した)")]
[Tooltip("行き先にこれだけ近づいたら着いたとみなす(m)")]
public float arriveRadius = 0.25f;
[Tooltip("経路の角にこれだけ近づいたら次の角へ進む(m)")]
public float cornerReachDistance = 0.12f;
[Tooltip("これより速度が小さければ「止まっている」とみなす(m/秒)")]
public float stoppedSpeedEpsilon = 0.02f;
[Header("止まったときに測る左右の空き")]
[Tooltip("左右を見る高さ(m)。椅子(0.45 m)に当たる高さにする")]
public float clearanceProbeHeight = 0.3f;
[Tooltip("左右を見る距離(m)。これより遠いものは見ない")]
public float clearanceProbeRange = 3.0f;
[Tooltip("道が引けないとき、ふさいでいるものを探す半径(m)")]
public float pathBlockSearchRadius = 2.0f;
[Tooltip("NavMeshAgent の譲り合いの優先度。小さいほど譲らない。人より小さくして人がよけるようにする")]
public int avoidancePriority = 0;
[Tooltip("経路を引き直しても見つからないとき、次に試すまでの間(秒)")]
public float repathRetrySec = 1.0f;
[Header("途中までの経路から出る(B7)")]
[Tooltip("止まっているあいだの「道が引けていなかった秒数(no_path_s)」がこれを超えたら、" +
"repathRetrySec を待たずに強く引き直す(秒)")]
public float noPathForceReplanSec = 10f;
[Tooltip("強く引き直してもまだ途中までしか引けないとき、いちど通路の入口を目指して出る")]
public bool escapeToAisleEntranceOnPartialPath = true;
public float Radius { get { return diameter * 0.5f; } }
}
}
Robot/ServingRobotController.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// 配膳ロボット1台。受け渡し口 → 卓 → 受け渡し口 の1往復を走り、
/// 止まる・動き出す・経路を引き直す・動けなくなるを自分で決める。
///
/// NavMeshAgent は経路を探すのと、人に自分をよけてもらうためだけに付ける。
/// 自動でよける機能(obstacleAvoidanceType)は切り、位置はこの script が動かす。
///
/// この script はロボットだけを扱う。人も店も動かさない。
/// 細い通路の区画と入口は、店の側で決めた値を StartTrip で受け取るだけ。
/// </summary>
[RequireComponent(typeof(NavMeshAgent))]
public class ServingRobotController : MonoBehaviour
{
public enum Phase
{
Idle, // 往復していない
ToTable, // 卓へ向かう(細い通路の入口で待つこともある)
Dwell, // 卓で25秒止まる(「止まった」に数えない)
ToPass, // 受け渡し口へ戻る
StuckWait, // 動けなくなって、人が来るのを待つ(この後、受け渡し口へ移して終わる)
Done // 往復が終わった
}
[Header("設定")]
public RobotSpec spec;
public FrontSensor sensor;
[Tooltip("1台めは1、2台めは2")]
public int robotId = 1;
[Header("いまの様子(読むだけ)")]
[SerializeField] Phase _phase = Phase.Idle;
[SerializeField] bool _enteredAisle;
[SerializeField] bool _yieldHold;
// ---- 1往復の測りもの ----
public int TableId { get; private set; }
/// <summary>
/// この走りを始めた、店の中の時刻(秒)。始めた側が渡す。渡されなければ -1。
/// 「いつ始まったか」をロボット自身が持つ。外から覚えておくと、
/// 走り終わってすぐ次の仕事が入ったときに取りこぼす。
/// </summary>
public float RunStartS { get; private set; }
public float DistanceM { get; private set; }
public float TotalS { get; private set; }
public float StoppedS { get; private set; }
public int StopCount { get; private set; }
public float MaxDecelMps2 { get; private set; }
public float MinClearanceM { get; private set; }
public bool Stuck { get; private set; }
public bool MetRobot { get; private set; }
public bool Aborted { get; private set; }
/// <summary>1往復が打ち切り(maxTripSec 超え)で終わったか</summary>
public bool TimedOut { get; private set; }
/// <summary>細い通路の入口で、ほかの台を待った秒数(秒)。stop_durations_s には入れない</summary>
public float WaitAisleS { get; private set; }
/// <summary>直前の1コマで進んだ距離(m)。コマの粗さを確かめるのに使う</summary>
public float LastStepM { get; private set; }
/// <summary>止まった原因</summary>
public enum StopCause
{
Guest, // 歩く客とすれ違った
Staff, // スタッフとすれ違った
PersonStanding, // 人が立ち止まっていた
Chair, // 椅子
OtherRobot, // もう1台のロボット(Unity だけ)
NoPath, // 行き先への道が引けない(通路の幅が足りない)
AisleQueue, // 細い通路の順番待ち(Unity だけ)
Unknown // 分からない
}
/// <summary>どう動き出したか</summary>
public enum StopResolve
{
SelfResume, // 自分で動き出した
Replanned, // 経路を引き直した
Stuck, // 動けなくなった
AisleWait // 入口で待った
}
/// <summary>止まり1回ぶんの記録</summary>
public struct StopEvent
{
public float startTimeS; // 店の中の時刻(秒)
public Vector3 position; // 止まった位置
public float durationS; // 止まっていた秒数
public StopCause cause;
public string causeDetail; // 何に当たったか
public StopResolve resolve;
public float clearanceLeftM; // 左の空き(ロボットの外周から)
public float clearanceRightM; // 右の空き
public bool pathBlocked; // 行き先への道が引けていなかったか
// ---- 止まっている間、何で動けなかったかの内訳(秒)。B7 を調べるため ----
/// <summary>前のセンサーが「ふさがっている」と言っていた秒数</summary>
public float blockedS;
/// <summary>行き先への道が引けていなかった秒数</summary>
public float noPathS;
/// <summary>細い通路の入口で順番を待っていた秒数</summary>
public float yieldS;
/// <summary>ふさがりが解けたあと、動き出すまで待っていた秒数</summary>
public float clearWaitS;
/// <summary>止まっている間に経路を引き直した回数</summary>
public int replans;
}
readonly List<StopEvent> _stopEvents = new List<StopEvent>();
/// <summary>この往復で止まった1回ずつの記録</summary>
public IReadOnlyList<StopEvent> StopEvents { get { return _stopEvents; } }
readonly List<float> _stopDurations = new List<float>();
/// <summary>自分で動き出した止まりの長さ(秒)。動けなくなった分と、入口で待った分は入れない</summary>
public IReadOnlyList<float> StopDurationsS { get { return _stopDurations; } }
public Phase CurrentPhase { get { return _phase; } }
public bool TripFinished { get { return _phase == Phase.Done; } }
public bool HasEnteredAisle { get { return _enteredAisle; } }
/// <summary>いまの速さ(m/秒。字幕が読むだけ)</summary>
public float SpeedMps { get { return _speed; } }
/// <summary>細い通路の入口で待っている最中か(字幕が読むだけ)</summary>
public bool YieldHolding { get { return _yieldHold; } }
/// <summary>卓での受け渡しの残り秒数(字幕が読むだけ)</summary>
public float DwellRemainS
{
get { return spec != null ? Mathf.Max(0f, spec.tableDwellSec - _dwellTimer) : 0f; }
}
/// <summary>人が動かしに来るまでの残り秒数(字幕が読むだけ)</summary>
public float StuckWaitRemainS
{
get { return spec != null ? Mathf.Max(0f, spec.stuckRecoveryWaitSec - _stuckWaitTimer) : 0f; }
}
public Bounds AisleZone { get { return _aisleZone; } }
/// <summary>1走りで寄る卓1つぶん</summary>
public struct RunStop
{
public int tableId;
public Vector3 point; // ロボットが止まる位置(通路の上)
public Vector3 aisleEntrance; // その通路に入る前に待つ位置
public Bounds aisleZone; // 譲り合いの区画
}
readonly List<RunStop> _stops = new List<RunStop>();
readonly List<int> _tableIds = new List<int>();
int _stopIndex;
/// <summary>この走りで寄る卓の番号</summary>
public IReadOnlyList<int> TableIds { get { return _tableIds; } }
/// <summary>この走りで何卓に届けたか</summary>
public int StopsCompleted { get; private set; }
/// <summary>この走りで寄る卓の数</summary>
public int RunStopCount { get { return _stops.Count; } }
/// <summary>1往復が終わったときに呼ばれる</summary>
public event Action<ServingRobotController> TripCompleted;
// ---- いまシーンにいるロボット(譲り合いの判定に使う)----
static readonly List<ServingRobotController> _all = new List<ServingRobotController>();
public static IReadOnlyList<ServingRobotController> All { get { return _all; } }
// ---- 内部 ----
NavMeshAgent _agent;
NavMeshPath _path; // Awake で作る。フィールドの初期化で new すると
// 「InitializeNavMeshPath is not allowed to be called
// from a MonoBehaviour constructor」になるため
Vector3[] _corners = new Vector3[0];
int _cornerIndex;
Vector3 _destination;
Vector3 _destinationOnMesh;
bool _hasDestinationOnMesh;
Vector3 _tablePoint;
Vector3 _passPoint;
Vector3 _aisleEntrance;
Bounds _aisleZone;
bool _hasAisleZone;
float _speed;
float _dwellTimer;
float _clearTimer;
float _stoppedTimer; // いま続けて止まっている秒数
float _lastReplanAtStopped;
float _stuckWaitTimer;
float _repathCooldown;
bool _inStop;
float _currentStopS;
bool _inWait;
float _currentWaitS;
Vector3 _pathEndPoint;
bool _hasPathEnd;
bool _wasBlocked;
bool _hasBrakeCause;
StopCause _brakeCause;
string _brakeDetail = "";
string _aisleBlockerName = "";
bool _replannedInStop;
bool _recordingStop;
StopEvent _open;
bool _pathValid;
// ---- 途中までの経路から出る(B7)----
/// <summary>この止まりの中で、最後に強く引き直したときの no_path_s</summary>
float _lastEscapeAtNoPathS;
/// <summary>いま通路の入口へ出ようとしているか</summary>
bool _escapeToEntrance;
/// <summary>
/// ロボットが歩く NavMesh の種類(agentTypeID)。既定の 0。
/// NavMesh が人用とロボット用の2枚になったので、引くときは必ず種類を指定する
/// (指定しないと「どの種類か決められない」と警告が出て、思わぬ面を拾う)。
/// </summary>
public const int RobotAgentTypeId = 0;
static NavMeshQueryFilter RobotFilter
{
get
{
NavMeshQueryFilter f = new NavMeshQueryFilter();
f.agentTypeID = RobotAgentTypeId;
f.areaMask = NavMesh.AllAreas;
return f;
}
}
/// <summary>
/// stops.csv の cause_detail に書く印。
/// この印がある止まりは「途中までの経路に縛られて、引き直して出た」もの。
/// CSV を割らないように、区切りはカンマを使わない。
/// </summary>
public const string PartialPathMark = "途中までの経路";
void Awake()
{
_path = new NavMeshPath();
_agent = GetComponent<NavMeshAgent>();
ApplySpecToAgent();
}
void OnEnable()
{
if (!_all.Contains(this)) _all.Add(this);
}
void OnDisable()
{
_all.Remove(this);
}
void ApplySpecToAgent()
{
if (_agent == null || spec == null) return;
_agent.radius = spec.Radius;
_agent.height = spec.height;
_agent.speed = spec.maxSpeed;
_agent.acceleration = spec.acceleration;
_agent.angularSpeed = spec.turnSpeedDegPerSec;
// 自動でよける機能は使わない(実機のよけ方と違うため)
// ロボットはロボット用の面(半径 navAgentRadius で焼いたもの)を歩く
if (_agent.agentTypeID != RobotAgentTypeId) _agent.agentTypeID = RobotAgentTypeId;
_agent.obstacleAvoidanceType = ObstacleAvoidanceType.NoObstacleAvoidance;
_agent.avoidancePriority = spec.avoidancePriority;
// 位置と向きはこの script が動かす。agent は経路を探すのと、人に見てもらうためだけ
_agent.updatePosition = false;
_agent.updateRotation = false;
_agent.autoRepath = false;
// isStopped は NavMesh に乗っている agent にしか使えない
if (_agent.enabled && _agent.isOnNavMesh) _agent.isStopped = true;
}
/// <summary>受け渡し口に置き直して、次の往復を始める準備をする</summary>
/// <summary>
/// 仕事がないときの待機場所へ、ゆっくり下がる(B2)。
/// 往復ではないので、距離・止まり・動けなくなったの記録には数えない。
/// 止まる決まり(前のセンサー・譲り合い)もここでは働かせない。
/// すでにその場所の近くにいれば何もしない。
/// </summary>
public void GoStandby(Vector3 spot)
{
if (_phase != Phase.Idle && _phase != Phase.Done) return;
if (_agent == null || !Application.isPlaying) return;
if (!_agent.enabled || !_agent.isOnNavMesh) return;
Vector3 here = transform.position; here.y = 0f;
Vector3 to = spot; to.y = 0f;
if (Vector3.Distance(here, to) <= spec.arriveRadius) { _agent.isStopped = true; return; }
_agent.isStopped = false;
_agent.speed = spec.maxSpeed;
_agent.SetDestination(spot);
}
public void PlaceAt(Vector3 position, Quaternion rotation)
{
transform.SetPositionAndRotation(position, rotation);
_speed = 0f;
if (_agent == null) return;
if (!Application.isPlaying) return; // 編集モードでは NavMeshAgent を触らない
if (!_agent.enabled) _agent.enabled = true;
_agent.Warp(position);
if (_agent.isOnNavMesh)
{
_agent.isStopped = true;
_agent.nextPosition = position;
}
}
/// <summary>1往復を始める(細い通路の譲り合いなし。場面の再現などに使う)</summary>
public void StartTrip(int tableId, Vector3 tablePoint, Vector3 passPoint)
{
StartTrip(tableId, tablePoint, passPoint, passPoint, new Bounds());
}
/// <summary>1往復を始める(1卓だけ)</summary>
public void StartTrip(int tableId, Vector3 tablePoint, Vector3 passPoint,
Vector3 aisleEntrance, Bounds aisleZone)
{
RunStop s = new RunStop();
s.tableId = tableId;
s.point = tablePoint;
s.aisleEntrance = aisleEntrance;
s.aisleZone = aisleZone;
List<RunStop> one = new List<RunStop>();
one.Add(s);
StartRun(one, passPoint);
}
/// <summary>
/// 1走りを始める。受け渡し口を出て、渡された卓を順に回り、受け渡し口へ戻る。
/// 止まる決まり(前のセンサー・譲り合い・動けなくなったときの扱い)は1卓のときと同じ。
/// </summary>
public void StartRun(IList<RunStop> stops, Vector3 passPoint)
{
StartRun(stops, passPoint, -1f);
}
/// <summary>始めた時刻(店の中, 秒)も渡して1走りを始める。記録に使う</summary>
public void StartRun(IList<RunStop> stops, Vector3 passPoint, float startS)
{
RunStartS = startS;
_stops.Clear();
_tableIds.Clear();
if (stops != null)
{
for (int i = 0; i < stops.Count; i++)
{
_stops.Add(stops[i]);
_tableIds.Add(stops[i].tableId);
}
}
_stopIndex = 0;
StopsCompleted = 0;
if (_stops.Count == 0) { _phase = Phase.Idle; return; }
BeginRun(passPoint);
}
void BeginRun(Vector3 passPoint)
{
RunStop first = _stops[0];
int tableId = first.tableId;
Vector3 tablePoint = first.point;
Vector3 aisleEntrance = first.aisleEntrance;
Bounds aisleZone = first.aisleZone;
if (_path == null) _path = new NavMeshPath();
ApplySpecToAgent();
if (Application.isPlaying && _agent != null)
{
if (!_agent.enabled) _agent.enabled = true;
if (!_agent.isOnNavMesh) _agent.Warp(transform.position);
}
TableId = tableId;
_tablePoint = tablePoint;
_passPoint = passPoint;
_aisleEntrance = aisleEntrance;
_aisleZone = aisleZone;
_hasAisleZone = aisleZone.size.x > 0.01f && aisleZone.size.z > 0.01f;
DistanceM = 0f;
TotalS = 0f;
StoppedS = 0f;
StopCount = 0;
MaxDecelMps2 = 0f;
MinClearanceM = float.PositiveInfinity;
Stuck = false;
MetRobot = false;
Aborted = false;
TimedOut = false;
WaitAisleS = 0f;
_stopDurations.Clear();
_stopEvents.Clear();
_speed = 0f;
_dwellTimer = 0f;
_clearTimer = 0f;
_hasDestinationOnMesh = false;
_wasBlocked = false;
_hasBrakeCause = false;
_stoppedTimer = 0f;
_lastReplanAtStopped = 0f;
_stuckWaitTimer = 0f;
_repathCooldown = 0f;
_inStop = false;
_currentStopS = 0f;
_lastEscapeAtNoPathS = 0f;
_escapeToEntrance = false;
// 譲り合いを使わない/区画が無いときは、はじめから「入った」扱いにする
_enteredAisle = !(spec != null && spec.aisleYielding) || !_hasAisleZone;
_yieldHold = false;
_phase = Phase.ToTable;
SetDestination(_tablePoint, true);
}
/// <summary>次の卓へ向かう。もう無ければ受け渡し口へ戻る</summary>
void NextStopOrReturn()
{
if (_stopIndex + 1 < _stops.Count)
{
_stopIndex++;
RunStop s = _stops[_stopIndex];
TableId = s.tableId;
_tablePoint = s.point;
_aisleEntrance = s.aisleEntrance;
_aisleZone = s.aisleZone;
_hasAisleZone = s.aisleZone.size.x > 0.01f && s.aisleZone.size.z > 0.01f;
_enteredAisle = !(spec != null && spec.aisleYielding) || !_hasAisleZone;
_yieldHold = false;
_escapeToEntrance = false;
_lastEscapeAtNoPathS = 0f;
_phase = Phase.ToTable;
SetDestination(_tablePoint, true);
return;
}
_phase = Phase.ToPass;
_escapeToEntrance = false;
_lastEscapeAtNoPathS = 0f;
SetDestination(_passPoint, true);
}
/// <summary>往復を打ち切る</summary>
public void AbortTrip()
{
if (_phase == Phase.Idle || _phase == Phase.Done) return;
Aborted = true;
Finish();
}
void Update()
{
if (spec == null) return;
if (_phase == Phase.Idle || _phase == Phase.Done) return;
float dt = Time.deltaTime;
if (dt <= 0f) return;
LastStepM = 0f;
FrontSensor.Reading reading = sensor != null ? sensor.Sense() : new FrontSensor.Reading();
if (reading.sawRobot) MetRobot = true;
// 卓での25秒。測りの時間には入れない
if (_phase == Phase.Dwell)
{
_dwellTimer += dt;
if (_dwellTimer >= spec.tableDwellSec) NextStopOrReturn();
SyncAgent();
return;
}
TotalS += dt;
UpdateClearance();
// 1往復の打ち切り(この往復は timeout として1行に残す)
if (TotalS > spec.maxTripSec)
{
TimedOut = true;
PlaceAt(_passPoint, transform.rotation);
Finish();
return;
}
// 動けなくなって人を待っているあいだ。
// 15秒たったら「人が動かした」扱いにして、経路を走らせずに受け渡し口へ移し、
// この往復を stuck=1 で終える。移した分の道のりと時間は足さない。
if (_phase == Phase.StuckWait)
{
StoppedS += dt;
_stuckWaitTimer += dt;
if (_stuckWaitTimer >= spec.stuckRecoveryWaitSec)
{
PlaceAt(_passPoint, transform.rotation);
Finish();
return;
}
SyncAgent();
return;
}
// ---- 通路の入口へ出ている途中(B7)。着いたら、もう一度もとの行き先を目指す ----
UpdateEscape();
// ---- 細い通路の譲り合い ----
bool waitingAtEntrance = UpdateAisleYielding();
// ---- 走る・止まる ----
bool blocked = reading.blocked;
// 原因は、ブレーキを踏み始めた瞬間(前が塞がったと分かった瞬間)のものを覚えておく。
// 速さが0になるまで待つと、そのあいだに相手が通り過ぎて「分からない」になってしまう
if (blocked && !_wasBlocked)
{
_brakeCause = CauseOf(reading);
_brakeDetail = string.IsNullOrEmpty(reading.nearestName) ? "" : reading.nearestName;
_hasBrakeCause = true;
}
_wasBlocked = blocked;
if (blocked) _clearTimer = 0f;
else _clearTimer += dt;
// 行き先への道が引けていないときも止まる(進めないのに走っている扱いにしない)
bool hasPath = _corners.Length > 0 && _cornerIndex < _corners.Length;
bool wantMove = !blocked && _clearTimer >= spec.resumeAfterClearSec && !waitingAtEntrance && hasPath;
float target = wantMove ? spec.maxSpeed : 0f;
float prevSpeed = _speed;
if (target > _speed) _speed = Mathf.Min(target, _speed + spec.acceleration * dt);
else _speed = Mathf.Max(target, _speed - spec.deceleration * dt);
float decel = (prevSpeed - _speed) / dt;
if (decel > MaxDecelMps2) MaxDecelMps2 = decel;
if (_speed <= spec.stoppedSpeedEpsilon)
{
StoppedS += dt;
if (waitingAtEntrance)
{
// 譲り合いで入口に待っている分。止まった回数・長さには数えない
WaitAisleS += dt;
_stoppedTimer = 0f;
_lastReplanAtStopped = 0f;
if (!_inWait)
{
_inWait = true;
_currentWaitS = 0f;
BeginStopRecord(StopCause.AisleQueue,
_aisleBlockerName.Length > 0 ? "先に入っている台:" + _aisleBlockerName : "順番待ち", false);
}
_currentWaitS += dt;
_open.yieldS += dt;
}
else
{
if (_inWait) { _inWait = false; EndStopRecord(_currentWaitS, StopResolve.AisleWait); }
_stoppedTimer += dt;
if (!_inStop)
{
_inStop = true;
_currentStopS = 0f;
_replannedInStop = false;
_lastEscapeAtNoPathS = 0f; // no_path_s は止まりごとに数え直す(B7)
string detail;
StopCause cause;
if (!hasPath) cause = CauseOfNoPath(out detail);
else if (_hasBrakeCause)
{
cause = _brakeCause; // ブレーキを踏み始めたときに見たもの
detail = _brakeDetail;
}
else
{
cause = CauseOf(reading);
detail = string.IsNullOrEmpty(reading.nearestName) ? "" : reading.nearestName;
}
BeginStopRecord(cause, detail, !hasPath);
_hasBrakeCause = false;
}
_currentStopS += dt;
// 何で動けないかを秒数で分けて残す(B7 を調べるため)。
// 止まる決まりは変えない。数えるだけ
if (blocked) _open.blockedS += dt;
if (!hasPath) _open.noPathS += dt;
if (!blocked && hasPath && _clearTimer < spec.resumeAfterClearSec)
_open.clearWaitS += dt;
// 止まって10秒ごとに経路を引き直す
if (_stoppedTimer - _lastReplanAtStopped >= spec.replanAfterStoppedSec)
{
_lastReplanAtStopped = _stoppedTimer;
_replannedInStop = true;
_open.replans++;
SetDestination(_destination);
}
// 途中までの経路に縛られているとき(B7)。
// 「道が引けていなかった秒数」がしきい値を超えたら、
// 引き直しの間隔(repathRetrySec)を待たずに強く引き直す。
// それでも途中までしか引けなければ、いちど通路の入口を目指して出る。
if (!hasPath && !_escapeToEntrance &&
_open.noPathS - _lastEscapeAtNoPathS >= spec.noPathForceReplanSec)
{
EscapePartialPath();
}
// 60秒動けなかったら「動けなくなった」
if (_stoppedTimer >= spec.stuckAfterStoppedSec)
{
EnterStuck();
SyncAgent();
return;
}
}
}
else
{
if (_inWait) { _inWait = false; EndStopRecord(_currentWaitS, StopResolve.AisleWait); }
if (_inStop)
{
_inStop = false;
StopCount++;
_stopDurations.Add(_currentStopS);
EndStopRecord(_currentStopS,
_replannedInStop ? StopResolve.Replanned : StopResolve.SelfResume);
}
_stoppedTimer = 0f;
_lastReplanAtStopped = 0f;
}
FaceAndMove(dt);
SyncAgent();
CheckArrival();
}
/// <summary>
/// 細い通路に入る前の譲り合い。
/// 行き先の通路にもう1台がいたら、太い通路側の入口で待つ。
/// 戻り(ToPass)では譲らない(出ないと詰まるため)。
/// </summary>
bool UpdateAisleYielding()
{
if (_phase != Phase.ToTable) return false;
if (!spec.aisleYielding || !_hasAisleZone || _enteredAisle) return false;
if (ContainsXZ(_aisleZone, transform.position))
{
_enteredAisle = true;
if (_yieldHold)
{
_yieldHold = false;
SetDestination(_tablePoint, true);
}
return false;
}
if (MustWaitForAisle())
{
if (!_yieldHold)
{
_yieldHold = true;
SetDestination(_aisleEntrance, true);
}
}
else if (_yieldHold)
{
_yieldHold = false;
SetDestination(_tablePoint, true);
}
if (!_yieldHold) return false;
Vector3 a = transform.position; a.y = 0f;
Vector3 b = _aisleEntrance; b.y = 0f;
return Vector3.Distance(a, b) <= spec.arriveRadius;
}
/// <summary>
/// 途中までの経路に縛られた止まりから出る(B7)。
/// 1. 引き直しの間隔(repathRetrySec)を待たずに強く引き直す
/// 2. それでも最後までつながらなければ、いちど通路の入口を目指して出る
/// どちらも「経路の引き直し」なので、この止まりは resolved が「経路を引き直した」になる。
/// cause_detail には「途中までの経路」の印を足す。
/// </summary>
void EscapePartialPath()
{
_lastEscapeAtNoPathS = _open.noPathS;
_replannedInStop = true;
_open.replans++;
MarkPartialPath();
// 1. 間隔を無視して引き直す
SetDestination(_destination, true);
if (_pathValid && _cornerIndex < _corners.Length) return;
// 2. それでも途中まで。通路の入口へ出る
if (!spec.escapeToAisleEntranceOnPartialPath) return;
Vector3 here = transform.position; here.y = 0f;
Vector3 exit = _aisleEntrance; exit.y = 0f;
if (Vector3.Distance(here, exit) <= spec.arriveRadius) return;
_escapeToEntrance = true;
SetDestination(_aisleEntrance, true);
}
/// <summary>通路の入口へ出ている途中。着いたら、もとの行き先をもう一度目指す</summary>
void UpdateEscape()
{
if (!_escapeToEntrance) return;
Vector3 here = transform.position; here.y = 0f;
Vector3 exit = _aisleEntrance; exit.y = 0f;
if (Vector3.Distance(here, exit) > spec.arriveRadius) return;
_escapeToEntrance = false;
SetDestination(_phase == Phase.ToPass ? _passPoint : _tablePoint, true);
}
/// <summary>この止まりに「途中までの経路」の印をつける(1回だけ)</summary>
void MarkPartialPath()
{
if (!_recordingStop) return;
string d = _open.causeDetail;
if (!string.IsNullOrEmpty(d) && d.Contains(PartialPathMark)) return;
_open.causeDetail = string.IsNullOrEmpty(d) ? PartialPathMark : d + "/" + PartialPathMark;
}
bool MustWaitForAisle()
{
for (int i = 0; i < _all.Count; i++)
{
ServingRobotController o = _all[i];
if (o == null || o == this) continue;
// その通路の中にいる台がいれば入らない
if (ContainsXZ(_aisleZone, o.transform.position)) { _aisleBlockerName = o.name; return true; }
// 同じ通路を目指していて、まだ入っていない台。番号の小さい方が先に入る
if (o._phase == Phase.ToTable && !o._enteredAisle && o._hasAisleZone &&
SameZone(o._aisleZone, _aisleZone) && o.robotId < robotId)
{
_aisleBlockerName = o.name;
return true;
}
}
_aisleBlockerName = "";
return false;
}
static bool ContainsXZ(Bounds b, Vector3 p)
{
return p.x >= b.min.x && p.x <= b.max.x && p.z >= b.min.z && p.z <= b.max.z;
}
static bool SameZone(Bounds a, Bounds b)
{
Vector3 d = a.center - b.center;
d.y = 0f;
return d.sqrMagnitude < 0.0025f; // 5 cm 以内なら同じ通路
}
void EnterStuck()
{
if (Stuck) return; // 1往復で stuck は最大1回
Stuck = true;
if (_inStop) EndStopRecord(_currentStopS, StopResolve.Stuck);
_inStop = false;
// 動けなくなった分は「自分で動き出した止まり」には数えない
_inStop = false;
_currentStopS = 0f;
_stuckWaitTimer = 0f;
_escapeToEntrance = false;
_lastEscapeAtNoPathS = 0f;
_phase = Phase.StuckWait;
}
/// <summary>止まった原因を、前のセンサーが見たものから決める</summary>
StopCause CauseOf(FrontSensor.Reading r)
{
if (r.nearestIsRobot) return StopCause.OtherRobot;
if (r.nearestIsChair) return StopCause.Chair;
if (r.nearestIsPerson)
{
if (r.nearestPersonStanding) return StopCause.PersonStanding;
return r.nearestPersonKind == PersonKind.Staff ? StopCause.Staff : StopCause.Guest;
}
if (r.sawRobot) return StopCause.OtherRobot;
if (r.sawChair) return StopCause.Chair;
if (r.sawPerson) return StopCause.Guest;
return StopCause.Unknown;
}
/// <summary>
/// 行き先への道が引けないときの原因。
/// ふさいでいるもの(引かれた椅子・もう1台のロボット)が見つかれば、その原因にする。
/// 何も見つからないときだけ「通路の幅が足りない」にする。
/// </summary>
StopCause CauseOfNoPath(out string detail)
{
StopCause cause;
string name;
// 行き止まりの点 → 行き先 → 自分のまわり、の順に探す
if (_hasPathEnd && FindBlocker(_pathEndPoint, out cause, out name))
{
detail = "経路なし:" + name;
return cause;
}
if (FindBlocker(_destination, out cause, out name))
{
detail = "経路なし:" + name;
return cause;
}
if (FindBlocker(transform.position, out cause, out name))
{
detail = "経路なし:" + name;
return cause;
}
detail = "経路なし:ふさいでいるものが見つからない";
return StopCause.NoPath;
}
const int BlockMax = 32;
readonly Collider[] _blockHits = new Collider[BlockMax];
/// <summary>
/// その点のまわりで、道をふさぎうるもの(引かれた椅子・ほかのロボット)のうち
/// いちばん近いものを探す。見つかれば true。
/// </summary>
bool FindBlocker(Vector3 around, out StopCause cause, out string name)
{
cause = StopCause.NoPath;
name = "";
Vector3 center = new Vector3(around.x, spec.clearanceProbeHeight, around.z);
int n = Physics.OverlapSphereNonAlloc(center, spec.pathBlockSearchRadius, _blockHits,
~0, QueryTriggerInteraction.Ignore);
if (n > BlockMax) n = BlockMax;
float best = float.MaxValue;
for (int i = 0; i < n; i++)
{
Collider col = _blockHits[i];
if (col == null) continue;
Chair chair = col.GetComponentInParent<Chair>();
if (chair != null)
{
// 引かれていない椅子は NavMesh を削らないので、道をふさがない
if (!chair.IsPulled) continue;
float d = Vector3.Distance(center, chair.transform.position);
if (d < best)
{
best = d;
cause = StopCause.Chair;
name = chair.transform.parent != null
? chair.transform.parent.name + "/" + chair.name : chair.name;
}
continue;
}
ServingRobotController other = col.GetComponentInParent<ServingRobotController>();
if (other != null && other != this)
{
float d = Vector3.Distance(center, other.transform.position);
if (d < best)
{
best = d;
cause = StopCause.OtherRobot;
name = other.name;
}
}
}
return best < float.MaxValue;
}
void BeginStopRecord(StopCause cause, string detail, bool pathBlocked)
{
_open = new StopEvent();
_open.startTimeS = Time.time;
_open.position = transform.position;
_open.cause = cause;
_open.causeDetail = detail == null ? "" : detail;
_open.pathBlocked = pathBlocked;
float left, right;
MeasureSideClearance(out left, out right);
_open.clearanceLeftM = left;
_open.clearanceRightM = right;
_recordingStop = true;
}
void EndStopRecord(float durationS, StopResolve resolve)
{
if (!_recordingStop) return;
_recordingStop = false;
_open.durationS = durationS;
_open.resolve = resolve;
_stopEvents.Add(_open);
}
/// <summary>
/// 止まった位置での、左右の空き(ロボットの外周から壁・席・椅子まで)。
/// 人とロボットは数えない(動くもののため)。
/// </summary>
void MeasureSideClearance(out float left, out float right)
{
left = ProbeSide(-transform.right);
right = ProbeSide(transform.right);
}
const int ProbeMax = 16;
readonly RaycastHit[] _probe = new RaycastHit[ProbeMax];
float ProbeSide(Vector3 dir)
{
float range = spec.clearanceProbeRange;
Vector3 origin = transform.position + Vector3.up * spec.clearanceProbeHeight;
int n = Physics.RaycastNonAlloc(origin, dir, _probe, range, ~0, QueryTriggerInteraction.Ignore);
if (n > ProbeMax) n = ProbeMax;
float best = range;
for (int i = 0; i < n; i++)
{
Collider col = _probe[i].collider;
if (col == null) continue;
if (col.GetComponentInParent<PersonBody>() != null) continue;
if (col.GetComponentInParent<ServingRobotController>() != null) continue;
if (_probe[i].distance < best) best = _probe[i].distance;
}
return Mathf.Max(0f, best - spec.Radius);
}
void CheckArrival()
{
if (_yieldHold) return; // いまの行き先は通路の入口なので、着いても卓ではない
if (_escapeToEntrance) return; // 途中までの経路から出ている途中(B7)。行き先は通路の入口
Vector3 here = transform.position; here.y = 0f;
Vector3 goal = _hasDestinationOnMesh ? _destinationOnMesh : _destination; goal.y = 0f;
bool near = Vector3.Distance(here, goal) <= spec.arriveRadius;
// 最後までつながった経路の角を使い切ったら、着いたとみなす
bool consumed = _pathValid && _corners.Length > 0 && _cornerIndex >= _corners.Length;
if (!near && !consumed) return;
if (_phase == Phase.ToTable)
{
_phase = Phase.Dwell;
_dwellTimer = 0f;
_speed = 0f;
StopsCompleted = _stopIndex + 1; // この卓には届いた
}
else if (_phase == Phase.ToPass)
{
Finish();
}
}
void Finish()
{
_phase = Phase.Done;
_speed = 0f;
if (_inWait) { _inWait = false; EndStopRecord(_currentWaitS, StopResolve.AisleWait); }
if (_inStop)
{
_inStop = false;
StopCount++;
_stopDurations.Add(_currentStopS);
EndStopRecord(_currentStopS,
_replannedInStop ? StopResolve.Replanned : StopResolve.SelfResume);
}
if (float.IsInfinity(MinClearanceM)) MinClearanceM = -1f; // 人がいなかった
if (TripCompleted != null) TripCompleted(this);
}
void SetDestination(Vector3 point) { SetDestination(point, false); }
void SetDestination(Vector3 point, bool force)
{
// 引き直しの間隔を空ける。いまの経路は消さない
if (_repathCooldown > 0f && !force)
{
_destination = point;
return;
}
_repathCooldown = spec.repathRetrySec;
if (_path == null) _path = new NavMeshPath();
_destination = point;
_pathValid = false;
_corners = new Vector3[0];
_cornerIndex = 0;
NavMeshQueryFilter filter = RobotFilter;
NavMeshHit hit;
Vector3 from = transform.position;
if (NavMesh.SamplePosition(from, out hit, spec.Radius * 4f, filter)) from = hit.position;
Vector3 to = point;
if (NavMesh.SamplePosition(to, out hit, spec.Radius * 4f, filter)) to = hit.position;
_destinationOnMesh = to;
_hasDestinationOnMesh = true;
if (NavMesh.CalculatePath(from, to, filter, _path) && _path.corners.Length > 0)
{
_corners = _path.corners;
_cornerIndex = 0;
_pathValid = _path.status == NavMeshPathStatus.PathComplete;
if (!_pathValid && _corners.Length > 0)
{
_pathEndPoint = _corners[_corners.Length - 1];
_hasPathEnd = true;
}
else _hasPathEnd = false;
// 足もとの角は飛ばす
Vector3 here = transform.position; here.y = 0f;
while (_cornerIndex < _corners.Length)
{
Vector3 c = _corners[_cornerIndex]; c.y = 0f;
if (Vector3.Distance(here, c) > spec.cornerReachDistance) break;
_cornerIndex++;
}
}
}
void FaceAndMove(float dt)
{
if (_repathCooldown > 0f) _repathCooldown -= dt;
if (_cornerIndex >= _corners.Length)
{
if (_repathCooldown <= 0f) SetDestination(_destination);
return;
}
Vector3 here = transform.position;
Vector3 corner = _corners[_cornerIndex];
Vector3 to = corner - here; to.y = 0f;
float d = to.magnitude;
while (d <= spec.cornerReachDistance && _cornerIndex < _corners.Length - 1)
{
_cornerIndex++;
corner = _corners[_cornerIndex];
to = corner - here; to.y = 0f;
d = to.magnitude;
}
if (d > 0.0001f)
{
Vector3 dir = to / d;
// 向きを変える
Quaternion want = Quaternion.LookRotation(dir, Vector3.up);
transform.rotation = Quaternion.RotateTowards(transform.rotation, want, spec.turnSpeedDegPerSec * dt);
float step = Mathf.Min(_speed * dt, d);
if (step > 0f)
{
transform.position = here + dir * step;
DistanceM += step;
LastStepM = step;
}
}
if (d <= spec.cornerReachDistance && _cornerIndex >= _corners.Length - 1) _cornerIndex = _corners.Length;
}
void SyncAgent()
{
if (_agent == null || !_agent.enabled) return;
if (!_agent.isOnNavMesh)
{
_agent.Warp(transform.position);
return;
}
_agent.nextPosition = transform.position;
}
void UpdateClearance()
{
float myR = spec.Radius;
Vector3 me = transform.position; me.y = 0f;
IReadOnlyList<PersonBody> all = PersonBody.All;
for (int i = 0; i < all.Count; i++)
{
PersonBody p = all[i];
if (p == null || !p.walksInAisle) continue;
Vector3 q = p.transform.position; q.y = 0f;
float gap = Vector3.Distance(me, q) - myR - p.radius;
if (gap < MinClearanceM) MinClearanceM = gap;
}
}
/// <summary>経路が最後までつながっているか(引き直しに失敗しているときは false)</summary>
public bool PathComplete { get { return _pathValid; } }
void OnDrawGizmosSelected()
{
if (!_hasAisleZone) return;
Gizmos.color = _yieldHold ? new Color(1f, 0.8f, 0.1f, 0.6f) : new Color(0.2f, 0.9f, 0.5f, 0.35f);
Gizmos.DrawWireCube(new Vector3(_aisleZone.center.x, 0.05f, _aisleZone.center.z),
new Vector3(_aisleZone.size.x, 0.1f, _aisleZone.size.z));
Gizmos.DrawWireSphere(_aisleEntrance + Vector3.up * 0.05f, 0.25f);
}
}
}
People/GuestAgent.cs
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// 立ち上がって歩いている客1人。使い回しの体で、GroupManager から用事を1つずつ渡される。
/// 待合で待つ/ドリンクバーへ/トイレへ/店を出る
/// 座っている人はこの体を持たない(通路に出ないため)。
///
/// この script は人だけを扱う。どの用事をいつ出すかは GroupManager が決める。
/// </summary>
[RequireComponent(typeof(NavMeshAgent))]
public class GuestAgent : MonoBehaviour
{
/// <summary>用事</summary>
public enum Errand { Waiting, DrinkBar, Toilet, Leave, Pay }
/// <summary>いまの段</summary>
public enum Leg { Idle, Going, Dwelling, Returning }
[Header("設定")]
public PeopleSpec spec;
[Tooltip("あきらめたことを残す先(B1)。無くても動く")]
public ServiceOps ops;
[Header("いまの様子(読むだけ)")]
[SerializeField] Errand _errand;
[SerializeField] Leg _leg = Leg.Idle;
[SerializeField] float _legS; // いまの段に入ってから歩いた秒数
NavMeshAgent _agent;
PersonRobotYield _yield; // ロボットをよけている最中は「着いた」と見ない
Vector3 _target;
Vector3 _home;
float _dwellSec;
float _timer;
bool _returnHome;
System.Action _onDone;
Vector3 _pending;
bool _needsDestination;
int _tableId;
bool _gaveUp;
public Errand CurrentErrand { get { return _errand; } }
public Leg CurrentLeg { get { return _leg; } }
/// <summary>この体が着けずにあきらめたか(字幕と報告が読む)</summary>
public bool GaveUp { get { return _gaveUp; } }
/// <summary>どの卓の客か(記録用)。GroupManager が入れる</summary>
public void SetTable(int tableId) { _tableId = tableId; }
void Awake()
{
_agent = GetComponent<NavMeshAgent>();
_yield = GetComponent<PersonRobotYield>();
}
/// <summary>ロボットをよけている最中か(行き先がよけ場所にすり替わっている)</summary>
bool Yielding
{
get
{
if (_yield == null) _yield = GetComponent<PersonRobotYield>();
return _yield != null && _yield.Intervening;
}
}
/// <summary>体をその場所に置く(用事を渡す前に呼ぶ)</summary>
public void PlaceAt(Vector3 p)
{
if (_agent == null) _agent = GetComponent<NavMeshAgent>();
transform.position = p;
if (Application.isPlaying && _agent != null && _agent.enabled && _agent.isOnNavMesh)
_agent.Warp(p);
}
/// <summary>用事を1つ渡す</summary>
public void Begin(PeopleSpec peopleSpec, Errand kind, Vector3 target, float dwellSec,
bool returnHome, Vector3 home, System.Action onDone)
{
spec = peopleSpec;
_errand = kind;
_target = target;
_dwellSec = dwellSec;
_returnHome = returnHome;
_home = home;
_onDone = onDone;
_timer = 0f;
_legS = 0f;
_gaveUp = false;
if (_agent == null) _agent = GetComponent<NavMeshAgent>();
ApplySpec();
if (kind == Errand.Waiting)
{
_leg = Leg.Dwelling;
_dwellSec = float.MaxValue; // 迎えに来るまで立っている
_needsDestination = false;
return;
}
_leg = Leg.Going;
Go(_target);
}
/// <summary>用事を取り消して止める(体を返すとき)</summary>
public void Stop()
{
_leg = Leg.Idle;
_onDone = null;
_needsDestination = false;
if (_agent != null && _agent.enabled && _agent.isOnNavMesh) _agent.isStopped = true;
}
void ApplySpec()
{
if (_agent == null || spec == null) return;
_agent.radius = spec.personRadius;
_agent.height = spec.personHeight;
_agent.speed = spec.guestSpeed;
_agent.acceleration = spec.acceleration;
_agent.angularSpeed = spec.angularSpeedDegPerSec;
_agent.stoppingDistance = spec.stoppingDistance;
_agent.obstacleAvoidanceType = spec.avoidanceQuality;
_agent.avoidancePriority = spec.avoidancePriority;
_agent.autoRepath = true;
// 人は人用の NavMesh(半径 0.2 m)を歩く。ロボットは 0.25 m のまま
PeopleNav.ApplyAgentType(_agent);
// 客はレジの内側(店員側)に入れない
_agent.areaMask = PeopleNav.GuestAreaMask(spec);
}
void Go(Vector3 point)
{
_pending = point;
_needsDestination = true;
if (PeopleNav.EnsureReady(transform, _agent) && PeopleNav.Go(_agent, point))
_needsDestination = false;
}
void Update()
{
if (_leg == Leg.Idle || spec == null || _agent == null) return;
// 歩いている段では、着けないまま時間が過ぎたらあきらめる(B1)。
// NavMesh に乗れない・行き先を入れられないときも時間が進むように、
// 下の「まだ用意できていない」で戻る前に数える
if (_leg == Leg.Going || _leg == Leg.Returning)
{
_legS += Time.deltaTime;
if (GiveUpIfLate()) return;
}
if (!PeopleNav.EnsureReady(transform, _agent)) return;
if (_needsDestination && !PeopleNav.Go(_agent, _pending)) return;
_needsDestination = false;
if (_leg == Leg.Dwelling)
{
_timer += Time.deltaTime;
if (_timer < _dwellSec) return;
_timer = 0f;
if (_returnHome) { _leg = Leg.Returning; Go(_home); }
else Finish();
return;
}
// ロボットをよけている最中は、行き先がよけ場所にすり替わっているので
// 「着いた」と見てはいけない
if (Yielding) return;
if (_agent.pathPending) return;
if (_agent.remainingDistance > _agent.stoppingDistance + 0.05f) return;
if (_leg == Leg.Going)
{
if (_errand == Errand.Leave) { Finish(); return; }
_leg = Leg.Dwelling;
_timer = 0f;
}
else if (_leg == Leg.Returning)
{
Finish();
}
}
/// <summary>
/// 行き先に着けないまま時間が過ぎたとき。
/// 行き(Going)→ あきらめて席に戻る。帰る客は入口へ移す
/// 帰り(Returning)→ 席の位置へ移して座らせる
/// </summary>
bool GiveUpIfLate()
{
if (_leg == Leg.Going)
{
if (spec.guestGiveUpSec <= 0f || _legS < spec.guestGiveUpSec) return false;
_gaveUp = true;
Record(_target, spec.guestGiveUpSec, _returnHome ? "あきらめた" : "移した");
if (!_returnHome)
{
// 帰るところだった客は、入口へ移して出たことにする
PlaceAt(_target);
Finish();
return true;
}
_leg = Leg.Returning;
_legS = 0f;
Go(_home);
return true;
}
if (_leg == Leg.Returning)
{
if (spec.guestWarpHomeSec <= 0f || _legS < spec.guestWarpHomeSec) return false;
// 席にも戻れない。席の位置へ移して座らせる
_gaveUp = true;
Record(_home, spec.guestWarpHomeSec, "移した");
PlaceAt(_home);
Finish();
return true;
}
return false;
}
void Record(Vector3 to, float waitedS, string action)
{
if (ops == null) return;
ops.AddGiveUp("客", ErrandName(_errand), _tableId, to, transform.position, waitedS, action);
}
public static string ErrandName(Errand e)
{
switch (e)
{
case Errand.DrinkBar: return "ドリンクバー";
case Errand.Toilet: return "トイレ";
case Errand.Leave: return "退店";
case Errand.Pay: return "会計";
default: return "待合";
}
}
void Finish()
{
_leg = Leg.Idle;
System.Action cb = _onDone;
_onDone = null;
if (cb != null) cb();
}
}
}
People/PeopleNav.cs
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// 人の NavMeshAgent を、安全に使えるようにする小さな道具。
///
/// prefab では NavMeshAgent を切ってある。置いた直後に有効にすると
/// 「NavMesh に近くないので agent を作れません」の警告が出たり、
/// NavMesh に乗る前に SetDestination を呼んで
/// 「can only be called on an active agent that has been placed on a NavMesh」
/// のエラーが出るため、ここを通してから使う。
///
/// この script は人だけを扱う。
/// </summary>
public static class PeopleNav
{
/// <summary>NavMesh の上を探す距離(m)</summary>
const float SampleRadius = 2.0f;
/// <summary>
/// 人が歩く NavMesh の種類(agentTypeID)。NavMeshBaker が焼いたあとに入れる。
/// 人は半径 0.2 m の面、ロボットは 0.25 m の面を歩く。
/// 焼けていなければ 0(ロボットと同じ面)のまま。
/// </summary>
public static int PeopleAgentTypeId { get; private set; }
/// <summary>人用の面を探すための絞り込み</summary>
public static NavMeshQueryFilter Filter
{
get
{
NavMeshQueryFilter f = new NavMeshQueryFilter();
f.agentTypeID = PeopleAgentTypeId;
f.areaMask = NavMesh.AllAreas;
return f;
}
}
/// <summary>焼いたあとに NavMeshBaker が呼ぶ</summary>
public static void SetPeopleAgentType(int agentTypeId)
{
PeopleAgentTypeId = Mathf.Max(0, agentTypeId);
}
/// <summary>
/// 客が歩ける区画(レジの内側=店員側を外す)。
/// スタッフは NavMesh.AllAreas のまま。
/// </summary>
public static int GuestAreaMask(PeopleSpec spec)
{
if (spec == null || spec.registerBackArea <= 2) return NavMesh.AllAreas;
return NavMesh.AllAreas & ~(1 << spec.registerBackArea);
}
/// <summary>その agent を人用の面で歩かせる</summary>
public static void ApplyAgentType(NavMeshAgent agent)
{
if (agent == null) return;
if (agent.agentTypeID != PeopleAgentTypeId) agent.agentTypeID = PeopleAgentTypeId;
}
/// <summary>
/// agent を使える状態にする。
/// 編集モード(Play していないとき)は何もせず false を返す。
/// </summary>
public static bool EnsureReady(Transform tr, NavMeshAgent agent)
{
if (!Application.isPlaying) return false;
if (agent == null || tr == null) return false;
if (!agent.enabled)
{
// 有効にする前に NavMesh の上へ移す(警告を出さないため)
NavMeshHit hit;
if (NavMesh.SamplePosition(tr.position, out hit, SampleRadius, Filter))
tr.position = hit.position;
else
return false; // 近くに NavMesh が無いので、まだ有効にしない
ApplyAgentType(agent);
agent.enabled = true;
}
if (!agent.isActiveAndEnabled) return false;
if (!agent.isOnNavMesh)
{
NavMeshHit hit;
if (NavMesh.SamplePosition(tr.position, out hit, SampleRadius, Filter))
agent.Warp(hit.position);
}
return agent.isOnNavMesh;
}
/// <summary>
/// 行き先を指示する。指示できたら true。
/// EnsureReady が true を返したあとだけ呼ぶ。
/// </summary>
public static bool Go(NavMeshAgent agent, Vector3 point)
{
if (agent == null || !agent.isActiveAndEnabled || !agent.isOnNavMesh) return false;
NavMeshHit hit;
if (NavMesh.SamplePosition(point, out hit, 1.5f, Filter))
return agent.SetDestination(hit.position);
return agent.SetDestination(point);
}
}
}
People/PeopleSpawner.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// ずっといる人を置く:スタッフと、卓ごとの「座っている客」(椅子を引く仕掛け)。
/// 歩く客と待合の客は、1組ごとの流れに合わせて GroupManager が出し入れする。
///
/// 置き場所は StoreBuilder から読むだけで、店は作らない。
/// </summary>
public class PeopleSpawner : MonoBehaviour
{
[Header("設定")]
public PeopleSpec spec;
public ServiceSpec service;
[Tooltip("置き場所を読む先。店は作らない")]
public StoreBuilder store;
[Tooltip("卓の空き具合を読む先(空いている卓では椅子を引かない)")]
public ServiceOps ops;
[Header("部品")]
public GameObject staffPrefab;
public GameObject seatedGuestPrefab;
PeopleTargets _targets;
/// <summary>卓の番号(store.Tables と同じ並び)ごとの椅子の出し入れ</summary>
readonly List<SeatedGuest> _seated = new List<SeatedGuest>();
public int StaffSpawned { get; private set; }
public int SeatedSpawned { get; private set; }
public PeopleTargets Targets { get { return _targets; } }
/// <summary>その卓の椅子の出し入れ(GroupManager が人の動きに合わせて呼ぶ)</summary>
public SeatedGuest SeatedAt(int tableIndex)
{
if (tableIndex < 0 || tableIndex >= _seated.Count) return null;
return _seated[tableIndex];
}
/// <summary>椅子を引いた回数の合計(run_info.json に出す)</summary>
public int ChairPullTotal
{
get
{
int n = 0;
for (int i = 0; i < _seated.Count; i++) if (_seated[i] != null) n += _seated[i].Pulls;
return n;
}
}
/// <summary>スタッフと、卓ごとの座っている客を置く</summary>
public void Spawn(int seed)
{
if (spec == null || store == null)
{
Debug.LogError("[ServiceSim] PeopleSpawner に PeopleSpec か StoreBuilder が入っていません");
return;
}
Clear();
_targets = CollectTargets();
System.Random rng = new System.Random(seed);
for (int i = 0; i < spec.staffCount; i++)
{
GameObject go = Spawn(staffPrefab, "Staff" + (i + 1).ToString("00"),
Scatter(_targets.pantry, 0.8f, rng), true);
StaffAgent a = go.GetComponent<StaffAgent>();
if (a != null) a.Configure(spec, service, ops, _targets, seed * 1000 + i, i);
AttachRobotYield(go, seed * 1000 + 100 + i);
StaffSpawned++;
}
IList<StoreBuilder.TableInfo> tables = store.Tables;
for (int i = 0; i < tables.Count; i++)
{
StoreBuilder.TableInfo t = tables[i];
GameObject go = Spawn(seatedGuestPrefab, "SeatedGuest_T" + t.tableId.ToString("00"), t.center, false);
SeatedGuest s = go.GetComponent<SeatedGuest>();
if (s != null) s.Configure(spec, t.aisleChairs, seed * 1000 + 2000 + t.tableId, ops, i);
_seated.Add(s);
PersonBody body = go.GetComponent<PersonBody>();
if (body != null)
{
body.radius = spec.personRadius;
body.walksInAisle = false;
body.kind = PersonKind.Seated;
}
SeatedSpawned++;
}
}
/// <summary>
/// 人がロボットをよける仕掛けを付ける(prefab に無くても付く)。
/// 歩く客の体は GroupManager が出し入れするので、そちらでも同じことをする。
/// </summary>
public void AttachRobotYield(GameObject go, int seed)
{
if (go == null) return;
PersonRobotYield y = go.GetComponent<PersonRobotYield>();
if (y == null) y = go.AddComponent<PersonRobotYield>();
y.Configure(spec, store, ops, seed);
}
public void Clear()
{
StaffSpawned = 0;
SeatedSpawned = 0;
_targets = null;
_seated.Clear();
for (int i = transform.childCount - 1; i >= 0; i--)
{
GameObject go = transform.GetChild(i).gameObject;
if (Application.isPlaying) Destroy(go);
else DestroyImmediate(go);
}
}
/// <summary>店から行き先を読む</summary>
public PeopleTargets CollectTargets()
{
PeopleTargets t = new PeopleTargets();
t.foodPass = store.PassPoint;
t.dishReturn = store.ReturnPoint;
t.pantry = store.PantryPoint;
t.register = store.RegisterPoint;
t.registerBack = store.RegisterBackPoint;
t.toilet = store.ToiletPoint;
t.drinkBar = store.DrinkBarPoint;
t.entrance = store.EntrancePoint;
t.waiting = store.WaitingPoint;
t.waitingArea = store.WaitingArea;
t.staffStandby.AddRange(store.StaffStandbyPoints);
IList<StoreBuilder.TableInfo> tables = store.Tables;
for (int i = 0; i < tables.Count; i++) t.tablePoints.Add(tables[i].deliveryPoint);
// 店に無い場所は、受け渡し口で代わりにする(格子型の店など)
if (!store.HasPoint(PlaceKind.DishReturn)) t.dishReturn = t.foodPass;
if (!store.HasPoint(PlaceKind.Pantry)) t.pantry = t.foodPass;
if (!store.HasPoint(PlaceKind.Entrance)) t.entrance = t.foodPass;
if (!store.HasPoint(PlaceKind.RegisterBack)) t.registerBack = t.register;
if (!store.HasPoint(PlaceKind.Waiting)) t.waiting = t.register;
if (!store.HasPoint(PlaceKind.Toilet)) t.toilet = t.register;
if (!store.HasPoint(PlaceKind.DrinkBar)) t.drinkBar = t.register;
return t;
}
GameObject Spawn(GameObject prefab, string name, Vector3 position, bool snapToNavMesh)
{
GameObject go;
if (prefab != null) go = Instantiate(prefab, transform);
else
{
go = new GameObject(name);
go.transform.SetParent(transform, false);
}
go.name = name;
Vector3 p = position;
if (snapToNavMesh)
{
NavMeshHit hit;
if (NavMesh.SamplePosition(p, out hit, 2.0f, PeopleNav.Filter)) p = hit.position;
}
go.transform.position = p;
if (spec != null)
{
PersonBody body = go.GetComponent<PersonBody>();
if (body != null) body.radius = spec.personRadius;
}
return go;
}
Vector3 Scatter(Vector3 center, float range, System.Random rng)
{
float a = (float)(rng.NextDouble() * System.Math.PI * 2.0);
float r = (float)rng.NextDouble() * range;
return center + new Vector3(Mathf.Cos(a) * r, 0f, Mathf.Sin(a) * r);
}
}
}
People/PeopleSpec.asset
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 485ae95cc6b11479eaf59a14275b67cb, type: 3}
m_Name: PeopleSpec
m_EditorClassIdentifier:
personRadius: 0.2
personHeight: 1.7
staffCount: 2
staffSpeed: 1
staffTaskWeights:
- 1
- 1
- 1
- 1
- 1
staffDwellAtPassSec: 3
staffDwellAtTableSec: 5
staffDwellAtReturnSec: 5
staffDwellAtPantrySec: 5
staffDwellAtWaitingSec: 5
staffDwellAtRegisterSec: 30
guestCountByCrowd: 010000000400000008000000
guestSpeed: 0.8
guestTripWeights:
- 1
- 1
- 1
guestDwellAtPoiSec: 10
guestDwellAtRegisterSec: 20
guestDwellAtTableSec: 30
warmupOccupancy: 0.8
jamNarrowAfterSec: 1
jamNarrowRadius: 0.12
jamBackOffAfterSec: 3
jamStuckSpeed: 0.05
waitingCountByCrowd: 000000000200000005000000
waitingShuffleSec: 25
chairPullHoldSec: 30
chairPullDistance: 0.3
chairPullIdleMeanIntervalSec: 1800
guestGiveUpSec: 60
guestWarpHomeSec: 60
staffGiveUpSec: 60
staffGiveUpMaxTries: 3
registerBackArea: 3
avoidancePriority: 50
avoidanceQuality: 3
acceleration: 2
angularSpeedDegPerSec: 360
stoppingDistance: 0.3
robotYieldEnabled: 1
robotAisleLookAheadM: 3
robotAisleMarginM: 0.3
robotDetourExtraMaxM: 5
robotFaceToFaceM: 1.5
robotKeepClearM: 0.3
robotSideStepM: 0.5
robotBackOffSearchM: 4
robotPassedClearM: 0.5
robotYieldMaxSec: 20
robotCarelessChance: 0.02
robotContactGapM: 0.1
robotNudgeSpeedMps: 0.5
People/PeopleSpec.cs
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>混み具合の3段階</summary>
public enum CrowdLevel
{
Empty = 0, // 空いている
Normal = 1, // 普通
Busy = 2 // 混雑
}
/// <summary>スタッフの仕事</summary>
public enum StaffTask
{
Serve = 0, // 料理を運ぶ(受け渡し口 → 卓 → 受け渡し口)
Clear = 1, // 下げ膳(卓 → 返却口)
Pantry = 2, // パントリーに立ち寄る
Guide = 3, // 待合の客を卓へ案内する
Register = 4 // レジ
}
/// <summary>歩く客の行き先</summary>
public enum GuestTrip
{
Toilet = 0, // 卓 → トイレ → 卓
DrinkBar = 1, // 卓 → ドリンクバー → 卓
Exit = 2 // 卓 → レジ → 入口から出る
}
/// <summary>
/// 人の数字。人に関する数字はすべてこの .asset に置く。
/// </summary>
[CreateAssetMenu(menuName = "ServiceSim/People Spec", fileName = "PeopleSpec")]
public class PeopleSpec : ScriptableObject
{
[Header("体の大きさ(箱と円柱で作る)")]
[Tooltip("体の半径(m)")]
public float personRadius = 0.2f;
[Tooltip("身長(m)")]
public float personHeight = 1.7f;
[Header("スタッフ")]
[Tooltip("スタッフの人数")]
public int staffCount = 2;
[Tooltip("スタッフの歩く速さ(m/秒)")]
public float staffSpeed = 1.0f;
[Tooltip("仕事を選ぶ重み。[料理を運ぶ, 下げ膳, パントリー, 案内, レジ]。初期値は同じ割合")]
public float[] staffTaskWeights = { 1f, 1f, 1f, 1f, 1f };
[Tooltip("受け渡し口で料理を取る時間(秒)")]
public float staffDwellAtPassSec = 3f;
[Tooltip("卓で料理を出す・下げる時間(秒)")]
public float staffDwellAtTableSec = 5f;
[Tooltip("返却口に皿を戻す時間(秒)")]
public float staffDwellAtReturnSec = 5f;
[Tooltip("パントリーに立ち寄る時間(秒)")]
public float staffDwellAtPantrySec = 5f;
[Tooltip("待合で客に声をかける時間(秒)")]
public float staffDwellAtWaitingSec = 5f;
[Tooltip("レジにいる時間(秒)")]
public float staffDwellAtRegisterSec = 30f;
[Header("歩く客")]
[Tooltip("混み具合ごとの、歩く客の人数。[空いている, 普通, 混雑]")]
public int[] guestCountByCrowd = { 1, 4, 8 };
[Tooltip("歩く客の速さ(m/秒)")]
public float guestSpeed = 0.8f;
[Tooltip("行き先を選ぶ重み。[トイレ, ドリンクバー, 帰る]")]
public float[] guestTripWeights = { 1f, 1f, 1f };
[Tooltip("トイレ・ドリンクバーで過ごす時間(秒)")]
public float guestDwellAtPoiSec = 10f;
[Tooltip("レジで支払う時間(秒)")]
public float guestDwellAtRegisterSec = 20f;
[Tooltip("卓に戻って座っている時間(秒)")]
public float guestDwellAtTableSec = 30f;
[Header("慣らし(始めの埋まり方)")]
[Tooltip("始めるときに、食事の途中の組で埋めておく卓の割合")]
public float warmupOccupancy = 0.8f;
[Header("人どうしのすれ違い")]
[Tooltip("前に進めない状態がこの秒数続いたら、体を横に向ける(半径を細める)")]
public float jamNarrowAfterSec = 1.0f;
[Tooltip("体を横に向けたときの半径(m)")]
public float jamNarrowRadius = 0.12f;
[Tooltip("それでも抜けられないとき、下がるまでの秒数")]
public float jamBackOffAfterSec = 3.0f;
[Tooltip("これより遅ければ「前に進めない」とみなす(m/秒)")]
public float jamStuckSpeed = 0.05f;
[Header("待合に立つ客")]
[Tooltip("混み具合ごとの、待合に立つ客の人数。[空いている, 普通, 混雑]")]
public int[] waitingCountByCrowd = { 0, 2, 5 };
[Tooltip("待合で立ち止まったまま、位置を少し変えるまでの時間(秒)")]
public float waitingShuffleSec = 25f;
[Header("椅子を引く(人の動きに合わせて引く)")]
[Tooltip("引いた椅子をそのままにしておく時間(秒)。座る・立つときに引いて、この秒数で戻す")]
public float chairPullHoldSec = 30f;
[Tooltip("椅子を通路側へ引く距離(m)")]
public float chairPullDistance = 0.3f;
[Tooltip("食事中に何もなく引く分の、1卓あたりの平均の間隔(秒)。" +
"座る・立つときの引きとは別。0 以下なら引かない")]
public float chairPullIdleMeanIntervalSec = 1800f;
[Header("あきらめて、やり直す(B1)")]
[Tooltip("客:行き先へ向かってこの秒数で着けなければ、あきらめて席に戻る")]
public float guestGiveUpSec = 60f;
[Tooltip("客:席に戻るのにもこの秒数で着けなければ、席の位置へ移して座らせる")]
public float guestWarpHomeSec = 60f;
[Tooltip("スタッフ:行き先へ向かってこの秒数で着けなければ、仕事を板に戻す")]
public float staffGiveUpSec = 60f;
[Tooltip("スタッフ:同じ仕事をこの回数あきらめたら、その場所へ移して仕事を済ませる")]
public int staffGiveUpMaxTries = 3;
[Header("よけ方(人の側がよける)")]
[Tooltip("レジの内側(店員側)に使う NavMesh の区画の番号。" +
"客の NavMeshAgent はこの区画を外して歩くので、店員側に入れない。" +
"0=Walkable・1=Not Walkable・2=Jump は使えないので 3 以上にする")]
public int registerBackArea = 3;
[Tooltip("人の譲り合いの優先度。ロボットより大きくして、人がよけるようにする")]
public int avoidancePriority = 50;
[Tooltip("自動でよける機能の質。人はこれを使ってよい")]
public ObstacleAvoidanceType avoidanceQuality = ObstacleAvoidanceType.GoodQualityObstacleAvoidance;
[Tooltip("人の加速(m/秒^2)")]
public float acceleration = 2.0f;
[Tooltip("人の向きを変える速さ(度/秒)")]
public float angularSpeedDegPerSec = 360f;
[Tooltip("行き先にこれだけ近づいたら着いたとみなす(m)")]
public float stoppingDistance = 0.3f;
[Header("ロボットをよける(人の側がよける。値はすべて仮)")]
[Tooltip("人がロボットをよけるか。切ると前のとおり気にせず歩く(比べるときに使う)")]
public bool robotYieldEnabled = true;
[Tooltip("細い通路に入る前に、経路の先をどこまで見るか(m)")]
public float robotAisleLookAheadM = 3.0f;
[Tooltip("通路の中にロボットがいるとみなす、通路のまわりの余白(m)")]
public float robotAisleMarginM = 0.3f;
[Tooltip("回り道してよい遠回りの上限(m)。これを超えるなら入口で待つ")]
public float robotDetourExtraMaxM = 5.0f;
[Tooltip("細い通路の中で向かい合ったとき、これを下回ったら下がってよける(m。外周どうしの空き)")]
public float robotFaceToFaceM = 1.5f;
[Tooltip("広い通路ですれ違うときに空ける距離(m。ロボットの外周から)")]
public float robotKeepClearM = 0.3f;
[Tooltip("ロボットの通り道からはずれるときの、横にずれる距離(m)")]
public float robotSideStepM = 0.5f;
[Tooltip("下がる先(よけ場所)を探す距離(m)")]
public float robotBackOffSearchM = 4.0f;
[Tooltip("ロボットが通り過ぎたとみなす、向かい合う距離からの余裕(m)")]
public float robotPassedClearM = 0.5f;
[Tooltip("待つ・下がる・回り道をあきらめて進むまでの秒数(秒)。B1 のあきらめより短くする")]
public float robotYieldMaxSec = 20f;
[Tooltip("上のよけ方を守らない割合(1回の行動につき)。スマホを見ている客などの代わり")]
public float robotCarelessChance = 0.02f;
[Tooltip("ロボットの外周からこの距離まで近づいたら contacts.csv に残す(m)")]
public float robotContactGapM = 0.1f;
[Tooltip("よけるときの横移動の速さ(m/秒)")]
public float robotNudgeSpeedMps = 0.5f;
// ---- 読み出し ----
public int GuestCount(CrowdLevel crowd) { return Pick(guestCountByCrowd, crowd); }
public int WaitingCount(CrowdLevel crowd) { return Pick(waitingCountByCrowd, crowd); }
/// <summary>通路を歩く人の数(スタッフ+歩く客)。待合に立つ客は数えない</summary>
public int WalkerCount(CrowdLevel crowd) { return staffCount + GuestCount(crowd); }
static int Pick(int[] values, CrowdLevel crowd)
{
if (values == null || values.Length == 0) return 0;
int i = Mathf.Clamp((int)crowd, 0, values.Length - 1);
return values[i];
}
/// <summary>重みで仕事を選ぶ</summary>
public StaffTask PickStaffTask(System.Random rng)
{
int i = PickWeighted(staffTaskWeights, 5, rng);
return (StaffTask)i;
}
/// <summary>重みで行き先を選ぶ</summary>
public GuestTrip PickGuestTrip(System.Random rng)
{
int i = PickWeighted(guestTripWeights, 3, rng);
return (GuestTrip)i;
}
static int PickWeighted(float[] weights, int count, System.Random rng)
{
float total = 0f;
for (int i = 0; i < count; i++) total += WeightAt(weights, i);
if (total <= 0f) return rng.Next(count);
float r = (float)rng.NextDouble() * total;
for (int i = 0; i < count; i++)
{
r -= WeightAt(weights, i);
if (r <= 0f) return i;
}
return count - 1;
}
static float WeightAt(float[] weights, int i)
{
if (weights == null || i >= weights.Length) return 0f;
return Mathf.Max(0f, weights[i]);
}
}
}
People/PeopleTargets.cs
using System.Collections.Generic;
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 人が向かう先のまとめ。店の側(StoreBuilder)から読んで詰め、人の script に渡す。
/// この script は人だけを扱う(店は作らない)。
/// </summary>
public class PeopleTargets
{
public Vector3 foodPass; // 料理の受け渡し口の前
public Vector3 dishReturn; // 下げ膳の返却口の前
public Vector3 pantry; // パントリーの前
public Vector3 register; // レジの前
public Vector3 toilet; // トイレの中
public Vector3 drinkBar; // ドリンクバーの前
public Vector3 entrance; // 入口の内側
public Vector3 waiting; // 待合の中心
/// <summary>待合の広さ(待つ客を散らすのに使う)</summary>
public Bounds waitingArea;
/// <summary>レジの内側(スタッフが会計に立つ点。太い通路からだけ入れる)</summary>
public Vector3 registerBack;
/// <summary>手の空いたスタッフが待つ点(1人に1つ。太い通路の通り道をふさがない)</summary>
public readonly List<Vector3> staffStandby = new List<Vector3>();
/// <summary>卓の前(ロボットと同じ停止点)</summary>
public readonly List<Vector3> tablePoints = new List<Vector3>();
public Vector3 RandomTable(System.Random rng)
{
if (tablePoints.Count == 0) return foodPass;
return tablePoints[rng.Next(tablePoints.Count)];
}
/// <summary>待合の中のどこかに立つ位置</summary>
public Vector3 RandomWaitingSpot(System.Random rng)
{
Vector3 c = waitingArea.center;
Vector3 e = waitingArea.extents;
if (e.x <= 0.01f || e.z <= 0.01f) return waiting;
float m = 0.3f;
float x = c.x + (float)(rng.NextDouble() * 2.0 - 1.0) * Mathf.Max(0f, e.x - m);
float z = c.z + (float)(rng.NextDouble() * 2.0 - 1.0) * Mathf.Max(0f, e.z - m);
return new Vector3(x, 0f, z);
}
}
}
People/PersonBody.cs
using System.Collections.Generic;
using UnityEngine;
namespace ServiceSim
{
/// <summary>人の種類</summary>
public enum PersonKind
{
Staff, // スタッフ
Guest, // 歩く客
Waiting, // 待合に立つ客
Seated // 座っている客
}
/// <summary>
/// 人の体。スタッフ・歩く客・待合の客・座っている客に共通で付ける。
/// ロボットの前のセンサーと、人との最短距離の測り、止まった原因の切り分けに使う。
/// この script は人の見かけと速さだけを持ち、行き先は持たない。
/// </summary>
public class PersonBody : MonoBehaviour
{
[Tooltip("体の半径(m)")]
public float radius = 0.2f;
[Tooltip("通路を歩く人か。待合に立つ客と座っている客は false")]
public bool walksInAisle = true;
[Tooltip("人の種類。止まった原因を分けるのに使う")]
public PersonKind kind = PersonKind.Guest;
[Tooltip("これより遅ければ「立ち止まっている」とみなす(m/秒)")]
public float standingSpeed = 0.1f;
/// <summary>いまの速さ(m/秒)。位置の変わり方から出す</summary>
public float Speed { get; private set; }
/// <summary>
/// 直前の1コマで進んだ距離(m)。1コマを長くしたときのすり抜けの確かめに使う(B10)。
/// ロボットだけでなく人も見る(スタッフは 1.0 m/秒 なので 0.05 秒で 0.05 m 進む)。
/// </summary>
public float LastStepM { get; private set; }
/// <summary>立ち止まっているか</summary>
public bool IsStanding { get { return Speed < standingSpeed; } }
Vector3 _prev;
bool _hasPrev;
static readonly List<PersonBody> _all = new List<PersonBody>();
/// <summary>いまシーンにいる人の一覧(読むだけ)</summary>
public static IReadOnlyList<PersonBody> All { get { return _all; } }
void OnEnable()
{
if (!_all.Contains(this)) _all.Add(this);
_prev = transform.position;
_hasPrev = true;
Speed = 0f;
}
void OnDisable()
{
_all.Remove(this);
}
void Update()
{
float dt = Time.deltaTime;
if (dt <= 0f) return;
Vector3 p = transform.position;
if (_hasPrev)
{
Vector3 d = p - _prev;
d.y = 0f;
LastStepM = d.magnitude;
Speed = LastStepM / dt;
}
_prev = p;
_hasPrev = true;
}
/// <summary>種類の名前(報告に出す)</summary>
public string KindName()
{
switch (kind)
{
case PersonKind.Staff: return "スタッフ";
case PersonKind.Waiting: return "待合の客";
case PersonKind.Seated: return "座っている客";
default: return "客";
}
}
}
}
People/PersonRobotYield.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// 人がロボットをよける。スタッフと歩く客に付ける。
///
/// 実際の店で、人はロボットに自分から当たりに行かない。いまは人がロボットを
/// 気にせず歩き、細い通路でロボットを両側からはさんで押し合う場面が出ていたので、
/// 人の側がよけるようにする。
///
/// 1. 先を見て待つ・回り道する:細い通路に入る前に、その通路の中にロボットがいたら
/// 入らない。回り道で行けて遠回りが少しなら回り道、なければ通路の入口の
/// ロボットの通り道からはずれた場所で待つ
/// 2. 向かい合ったら下がる:細い通路の中で向かい合って近づいたら、
/// 近くの交差点か、壁ぞいの卓のすき間(よけ場所)に下がって、通り過ぎてから進む
/// 3. 間を空ける:広い通路ですれ違うときは、ロボットの外周から少し空ける
/// 4. まれな不注意:1回の行動につき決めた割合だけ、上を守らない人を混ぜる
///
/// ロボットのふるまいは変えない(NavMesh も経路も触らない)。
/// この script は人だけを扱う。数字はすべて PeopleSpec の .asset に置く。
/// </summary>
[RequireComponent(typeof(NavMeshAgent))]
public class PersonRobotYield : MonoBehaviour
{
/// <summary>いまのよけ方</summary>
public enum Mode
{
Clear, // ふつうに歩いている
WaitAtEntrance, // 細い通路の入口で待っている
Detour, // 回り道している
BackOff // 通路の中で下がってよけている
}
[Header("いまの様子(読むだけ)")]
[SerializeField] Mode _mode = Mode.Clear;
[SerializeField] bool _careless;
[SerializeField] float _modeS;
PeopleSpec _spec;
StoreBuilder _store;
ServiceOps _ops;
NavMeshAgent _agent;
PersonBody _body;
System.Random _rng;
Vector3 _ownerDest;
bool _hasOwnerDest;
Vector3 _goal; // 待つ・下がる・回り道の行き先
int _aisle = -1; // 相手のいる(または入ろうとしている)細い通路
bool _contactOpen;
NavMeshPath _path;
/// <summary>よけている最中か(GuestAgent・StaffAgent が「着いた」と間違えないように読む)</summary>
public bool Intervening { get { return _mode != Mode.Clear; } }
/// <summary>いまのよけ方(字幕が読む)</summary>
public Mode CurrentMode { get { return _mode; } }
/// <summary>この行動では1〜3を守らない人か(字幕と記録が読む)</summary>
public bool Careless { get { return _careless; } }
/// <summary>よけ始めてからの秒数(字幕が読む)</summary>
public float ModeSeconds { get { return _modeS; } }
void Awake()
{
_agent = GetComponent<NavMeshAgent>();
_body = GetComponent<PersonBody>();
_path = new NavMeshPath();
}
/// <summary>置いたあとに呼ぶ。人を出す側(PeopleSpawner・GroupManager)が入れる</summary>
public void Configure(PeopleSpec spec, StoreBuilder store, ServiceOps ops, int seed)
{
_spec = spec;
_store = store;
_ops = ops;
if (_rng == null) _rng = new System.Random(seed);
if (_agent == null) _agent = GetComponent<NavMeshAgent>();
if (_body == null) _body = GetComponent<PersonBody>();
if (_path == null) _path = new NavMeshPath();
Release();
_hasOwnerDest = false;
_contactOpen = false;
}
void Update()
{
if (_spec == null || _agent == null || _store == null) return;
if (!Application.isPlaying) return;
if (!_agent.isActiveAndEnabled || !_agent.isOnNavMesh) return;
float dt = Time.deltaTime;
if (dt <= 0f) return;
TrackOwnerDestination();
// 近づきすぎは、不注意の人もふくめて数える(記録は止めない)
LogContactIfClose();
if (!_spec.robotYieldEnabled || _careless)
{
if (Intervening) Release();
return;
}
_modeS += dt;
switch (_mode)
{
case Mode.Clear: UpdateClear(); break;
case Mode.WaitAtEntrance: UpdateWait(dt); break;
case Mode.Detour: UpdateDetour(); break;
case Mode.BackOff: UpdateBackOff(dt); break;
}
}
// ---------------- 行動の切り替わりを見る(不注意のくじを引き直す)----------------
/// <summary>
/// 持ち主(GuestAgent・StaffAgent)が行き先を変えたら「次の行動」とみなし、
/// 不注意かどうかを引き直す(1回の行動につき robotCarelessChance)。
/// </summary>
void TrackOwnerDestination()
{
if (Intervening) return; // よけている最中は自分が行き先を持っている
Vector3 d = _agent.destination;
if (_hasOwnerDest && (d - _ownerDest).sqrMagnitude < 0.01f) return;
_ownerDest = d;
_hasOwnerDest = true;
_careless = _rng != null && _rng.NextDouble() < _spec.robotCarelessChance;
}
// ---------------- ふつうに歩いているとき ----------------
void UpdateClear()
{
int here = NarrowAisleAt(transform.position, 0f);
if (here >= 0)
{
// 2. 細い通路の中。向かい合って近づいたら下がる。
// 通路の中では横に押さない(壁に押し付けることになる)
ServingRobotController r = FacingRobotInAisle(here);
if (r != null && GapTo(r) < _spec.robotFaceToFaceM) BeginBackOff(here, r);
return;
}
// 3. 広い通路では、ロボットの外周から少し空ける
KeepClear();
// 1. これから入る細い通路にロボットがいたら、入らない
int next = AisleAboutToEnter();
if (next < 0) return;
if (RobotInAisle(next) == null) return;
if (TryDetour(next)) return;
BeginWait(next);
}
/// <summary>
/// 3. 広い通路ですれ違うとき、ロボットの外周から robotKeepClearM 空ける。
/// NavMeshAgent.Move で横にずらすだけなので、経路も NavMesh も変えない。
/// </summary>
void KeepClear()
{
ServingRobotController r = NearestRobot();
if (r == null) return;
float need = _spec.personRadius + RobotRadius(r) + _spec.robotKeepClearM;
Vector3 d = transform.position - r.transform.position;
d.y = 0f;
float dist = d.magnitude;
if (dist >= need || dist < 0.0001f) return;
float step = Mathf.Min(_spec.robotNudgeSpeedMps * Time.deltaTime, need - dist);
_agent.Move(d / dist * step);
}
// ---------------- 1. 入口で待つ ----------------
void BeginWait(int aisle)
{
_aisle = aisle;
_mode = Mode.WaitAtEntrance;
_modeS = 0f;
_goal = SideStepPoint(aisle);
if (_ops != null) _ops.AddRobotYieldWait();
PeopleNav.Go(_agent, _goal);
}
void UpdateWait(float dt)
{
if (_ops != null) _ops.AddRobotYieldWaitSeconds(dt);
if (RobotInAisle(_aisle) == null) { Release(); return; }
if (_modeS >= _spec.robotYieldMaxSec) { Release(); return; } // 待ちすぎたら進む
}
// ---------------- 1. 回り道 ----------------
/// <summary>
/// ふさがれた通路のかわりに、別の細い通路を通って行けるか。
/// 遠回りが robotDetourExtraMaxM 以内で、その通路にロボットがいなければ通る。
/// </summary>
bool TryDetour(int blocked)
{
if (!_hasOwnerDest) return false;
if (_spec.robotDetourExtraMaxM <= 0f) return false;
float direct = PathLength(transform.position, _ownerDest);
if (direct < 0f) return false;
IList<StoreBuilder.AisleInfo> aisles = _store.Aisles;
int best = -1;
float bestExtra = float.MaxValue;
Vector3 bestPoint = Vector3.zero;
for (int i = 0; i < aisles.Count; i++)
{
if (i == blocked || !aisles[i].narrow) continue;
if (RobotInAisle(i) != null) continue;
Vector3 via = new Vector3(aisles[i].zone.center.x, 0f, aisles[i].zone.center.z);
float a = PathLength(transform.position, via);
if (a < 0f) continue;
float b = PathLength(via, _ownerDest);
if (b < 0f) continue;
float extra = a + b - direct;
if (extra > _spec.robotDetourExtraMaxM) continue;
if (extra >= bestExtra) continue;
best = i; bestExtra = extra; bestPoint = via;
}
if (best < 0) return false;
_aisle = best;
_mode = Mode.Detour;
_modeS = 0f;
_goal = bestPoint;
if (_ops != null) _ops.AddRobotYieldDetour();
PeopleNav.Go(_agent, _goal);
return true;
}
void UpdateDetour()
{
// 回り道の通路にロボットが入ってきたら、やめて考え直す
if (RobotInAisle(_aisle) != null) { Release(); return; }
if (_modeS >= _spec.robotYieldMaxSec) { Release(); return; }
Vector3 a = transform.position; a.y = 0f;
Vector3 b = _goal; b.y = 0f;
if (Vector3.Distance(a, b) <= _spec.stoppingDistance + 0.1f) Release();
}
// ---------------- 2. 通路の中で下がってよける ----------------
void BeginBackOff(int aisle, ServingRobotController r)
{
_aisle = aisle;
_mode = Mode.BackOff;
_modeS = 0f;
_goal = FindRefuge(aisle, r);
if (_ops != null) _ops.AddRobotYieldWait();
PeopleNav.Go(_agent, _goal);
}
void UpdateBackOff(float dt)
{
if (_ops != null) _ops.AddRobotYieldWaitSeconds(dt);
ServingRobotController r = NearestRobotInAisle(_aisle);
if (r == null) { Release(); return; }
// 通り過ぎたか(よけ場所から離れていったか)
if (GapTo(r) > _spec.robotFaceToFaceM + _spec.robotPassedClearM) { Release(); return; }
if (_modeS >= _spec.robotYieldMaxSec) { Release(); return; }
}
/// <summary>
/// 下がる先。壁ぞいの卓のすき間など、店が持っている「よけ場所」のうち
/// いちばん近いもの。無ければ、その通路のロボットと反対側の出口。
/// ロボットの側(いまより近づく方向)には行かない。
/// </summary>
Vector3 FindRefuge(int aisle, ServingRobotController r)
{
Vector3 me = transform.position; me.y = 0f;
Vector3 rp = r.transform.position; rp.y = 0f;
float toRobot = Vector3.Distance(me, rp);
IList<Vector3> refuges = _store.Refuges;
Vector3 best = Vector3.zero;
float bestD = float.MaxValue;
for (int i = 0; i < refuges.Count; i++)
{
Vector3 p = refuges[i]; p.y = 0f;
float d = Vector3.Distance(me, p);
if (d > _spec.robotBackOffSearchM) continue;
if (Vector3.Distance(p, rp) < toRobot) continue; // ロボットの側へは下がらない
if (d >= bestD) continue;
best = refuges[i]; bestD = d;
}
if (bestD < float.MaxValue) return Snap(best);
return Snap(AisleExitAwayFrom(aisle, rp));
}
/// <summary>その通路の、ロボットと反対側の出口のすぐ外</summary>
Vector3 AisleExitAwayFrom(int aisle, Vector3 robotPos)
{
StoreBuilder.AisleInfo a = _store.Aisles[aisle];
Bounds z = a.zone;
bool northSouth = z.size.z >= z.size.x;
float out1 = _spec.robotSideStepM;
if (northSouth)
{
bool goNorth = robotPos.z < z.center.z;
float zz = goNorth ? z.max.z + out1 : z.min.z - out1;
float sign = transform.position.x >= z.center.x ? 1f : -1f;
return new Vector3(z.center.x + sign * out1, 0f, zz);
}
bool goEast = robotPos.x < z.center.x;
float xx = goEast ? z.max.x + out1 : z.min.x - out1;
float signZ = transform.position.z >= z.center.z ? 1f : -1f;
return new Vector3(xx, 0f, z.center.z + signZ * out1);
}
/// <summary>通路の入口で、ロボットの通り道からはずれた場所</summary>
Vector3 SideStepPoint(int aisle)
{
Bounds z = _store.Aisles[aisle].zone;
bool northSouth = z.size.z >= z.size.x;
Vector3 p = transform.position;
if (northSouth)
{
float d = p.x - z.center.x;
float sign = Mathf.Abs(d) < 0.01f ? 1f : Mathf.Sign(d);
p += new Vector3(sign * _spec.robotSideStepM, 0f, 0f);
}
else
{
float d = p.z - z.center.z;
float sign = Mathf.Abs(d) < 0.01f ? 1f : Mathf.Sign(d);
p += new Vector3(0f, 0f, sign * _spec.robotSideStepM);
}
return Snap(p);
}
void Release()
{
_mode = Mode.Clear;
_modeS = 0f;
_aisle = -1;
if (_hasOwnerDest) PeopleNav.Go(_agent, _ownerDest);
}
// ---------------- 通路とロボットを見る ----------------
/// <summary>その点が入っている細い通路。無ければ -1</summary>
int NarrowAisleAt(Vector3 p, float margin)
{
IList<StoreBuilder.AisleInfo> aisles = _store.Aisles;
for (int i = 0; i < aisles.Count; i++)
{
if (!aisles[i].narrow) continue;
if (ContainsXZ(aisles[i].zone, p, margin)) return i;
}
return -1;
}
/// <summary>
/// これから入る細い通路。いまの経路を少し先まで見て、
/// 「いま入っていない細い通路」に入るならその番号を返す。
/// </summary>
int AisleAboutToEnter()
{
if (_agent.pathPending) return -1;
Vector3[] corners = _agent.path != null ? _agent.path.corners : null;
if (corners == null || corners.Length == 0) return -1;
float budget = _spec.robotAisleLookAheadM;
const float Step = 0.25f;
Vector3 from = transform.position;
for (int i = 0; i < corners.Length && budget > 0f; i++)
{
Vector3 to = corners[i];
Vector3 d = to - from; d.y = 0f;
float len = d.magnitude;
if (len > 0.0001f)
{
Vector3 dir = d / len;
for (float t = Step; t <= Mathf.Min(len, budget); t += Step)
{
int a = NarrowAisleAt(from + dir * t, 0f);
if (a >= 0 && !ContainsXZ(_store.Aisles[a].zone, transform.position, 0f)) return a;
}
}
budget -= len;
from = to;
}
return -1;
}
/// <summary>その通路の中にいるロボット(向かってくる・止まっているを問わない)</summary>
ServingRobotController RobotInAisle(int aisle)
{
if (aisle < 0 || aisle >= _store.Aisles.Count) return null;
Bounds z = _store.Aisles[aisle].zone;
IReadOnlyList<ServingRobotController> all = ServingRobotController.All;
for (int i = 0; i < all.Count; i++)
{
ServingRobotController r = all[i];
if (r == null) continue;
if (ContainsXZ(z, r.transform.position, _spec.robotAisleMarginM)) return r;
}
return null;
}
ServingRobotController NearestRobotInAisle(int aisle)
{
if (aisle < 0 || aisle >= _store.Aisles.Count) return null;
Bounds z = _store.Aisles[aisle].zone;
ServingRobotController best = null;
float bestD = float.MaxValue;
IReadOnlyList<ServingRobotController> all = ServingRobotController.All;
for (int i = 0; i < all.Count; i++)
{
ServingRobotController r = all[i];
if (r == null) continue;
if (!ContainsXZ(z, r.transform.position, _spec.robotAisleMarginM)) continue;
float d = Flat(r.transform.position - transform.position).magnitude;
if (d >= bestD) continue;
best = r; bestD = d;
}
return best;
}
/// <summary>その通路の中で、自分の進む先にいるロボット(向かい合っている)</summary>
ServingRobotController FacingRobotInAisle(int aisle)
{
ServingRobotController r = NearestRobotInAisle(aisle);
if (r == null) return null;
Vector3 ahead = Flat(_agent.desiredVelocity);
if (ahead.sqrMagnitude < 0.0004f) ahead = Flat(transform.forward);
if (ahead.sqrMagnitude < 0.0004f) return r; // 向きが分からないときは相手とみなす
Vector3 toRobot = Flat(r.transform.position - transform.position);
return Vector3.Dot(ahead.normalized, toRobot.normalized) > 0f ? r : null;
}
ServingRobotController NearestRobot()
{
ServingRobotController best = null;
float bestD = float.MaxValue;
IReadOnlyList<ServingRobotController> all = ServingRobotController.All;
for (int i = 0; i < all.Count; i++)
{
ServingRobotController r = all[i];
if (r == null) continue;
float d = Flat(r.transform.position - transform.position).magnitude;
if (d >= bestD) continue;
best = r; bestD = d;
}
return best;
}
/// <summary>ロボットの外周から自分の外周までの空き(m)。負なら重なっている</summary>
float GapTo(ServingRobotController r)
{
float d = Flat(r.transform.position - transform.position).magnitude;
return d - RobotRadius(r) - _spec.personRadius;
}
float RobotRadius(ServingRobotController r)
{
return r != null && r.spec != null ? r.spec.Radius : 0.25f;
}
// ---------------- 近づきすぎの記録 ----------------
/// <summary>
/// ロボットの外周 robotContactGapM 以内に近づいた回数を残す(接触に近い)。
/// 1回の近づきで1行。いちど離れるまで同じ近づきを数えない。
/// </summary>
void LogContactIfClose()
{
ServingRobotController r = NearestRobot();
if (r == null) { _contactOpen = false; return; }
float gap = GapTo(r);
if (!_contactOpen && gap <= _spec.robotContactGapM)
{
_contactOpen = true;
if (_ops != null)
_ops.AddRobotContact(transform.position, WhoName(), _careless, r.robotId, gap);
}
else if (_contactOpen && gap > _spec.robotContactGapM + _spec.robotKeepClearM)
{
_contactOpen = false;
}
}
string WhoName()
{
string kind = _body != null ? _body.KindName() : "人";
return kind + ":" + name;
}
// ---------------- 小さな道具 ----------------
float PathLength(Vector3 from, Vector3 to)
{
if (_path == null) _path = new NavMeshPath();
NavMeshQueryFilter f = PeopleNav.Filter;
NavMeshHit hit;
Vector3 a = from, b = to;
if (NavMesh.SamplePosition(a, out hit, 1.5f, f)) a = hit.position;
if (NavMesh.SamplePosition(b, out hit, 1.5f, f)) b = hit.position;
if (!NavMesh.CalculatePath(a, b, f, _path)) return -1f;
if (_path.status != NavMeshPathStatus.PathComplete) return -1f;
float len = 0f;
Vector3[] c = _path.corners;
for (int i = 1; i < c.Length; i++) len += Vector3.Distance(c[i - 1], c[i]);
return len;
}
static Vector3 Snap(Vector3 p)
{
NavMeshHit hit;
if (NavMesh.SamplePosition(p, out hit, 1.5f, PeopleNav.Filter)) return hit.position;
return p;
}
static Vector3 Flat(Vector3 v) { v.y = 0f; return v; }
static bool ContainsXZ(Bounds b, Vector3 p, float margin)
{
return p.x >= b.min.x - margin && p.x <= b.max.x + margin &&
p.z >= b.min.z - margin && p.z <= b.max.z + margin;
}
}
}
People/SeatedGuest.cs
using System.Collections.Generic;
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 1卓ぶんの椅子の出し入れ。座っている人は人として置かないので、
/// この script が「その卓の椅子」だけを持つ。
///
/// 引くきっかけは人の動きに合わせる(B6)。GroupManager が次のときに呼ぶ。
/// 座るとき/ドリンクバーへ立つとき/トイレへ立つとき/会計で立つとき/退店するとき/戻って座り直すとき
/// 食事中に何もなく引く分は、別の長い間隔(spec.chairPullIdleMeanIntervalSec)で少しだけ起こす。
///
/// この script は椅子の出し入れだけを扱う。いつ引くかは GroupManager が決め、
/// 椅子1脚の状態は Chair が持つ。数字は PeopleSpec の .asset に置く。
/// </summary>
public class SeatedGuest : MonoBehaviour
{
/// <summary>引いたきっかけ(記録に出す)</summary>
public const string ReasonSit = "座る";
public const string ReasonDrinkBar = "ドリンクバー";
public const string ReasonToilet = "トイレ";
public const string ReasonPay = "会計";
public const string ReasonLeave = "退店";
public const string ReasonSitBack = "戻って座る";
public const string ReasonIdle = "食事中";
[Header("設定")]
public PeopleSpec spec;
[Header("いまの様子(読むだけ)")]
[SerializeField] int _pulledNow;
[SerializeField] float _nextIdlePullIn;
[SerializeField] int _pulls;
readonly List<Chair> _aisleChairs = new List<Chair>();
/// <summary>_aisleChairs と同じ並び。0 より大きければ、その秒数だけ引いたまま</summary>
readonly List<float> _holdLeft = new List<float>();
System.Random _rng;
ServiceOps _ops;
int _tableIndex = -1;
/// <summary>いま1脚でも引いているか</summary>
public bool IsPulling { get { return _pulledNow > 0; } }
/// <summary>この卓で引いた回数</summary>
public int Pulls { get { return _pulls; } }
/// <summary>引ける椅子の数</summary>
public int AisleChairCount { get { return _aisleChairs.Count; } }
/// <summary>この卓の引ける椅子と、乱数の種を渡す</summary>
public void Configure(PeopleSpec peopleSpec, IList<Chair> aisleChairs, int seed,
ServiceOps ops, int tableIndex)
{
_ops = ops;
_tableIndex = tableIndex;
spec = peopleSpec;
_aisleChairs.Clear();
_holdLeft.Clear();
if (aisleChairs != null)
{
for (int i = 0; i < aisleChairs.Count; i++)
{
if (aisleChairs[i] == null) continue;
_aisleChairs.Add(aisleChairs[i]);
_holdLeft.Add(0f);
}
}
_rng = new System.Random(seed);
_pulledNow = 0;
_pulls = 0;
_nextIdlePullIn = NextIdleInterval();
}
/// <summary>
/// 人が動いたので、その人の席の椅子を引く。
/// seatOrder はその卓の何人目か(同じ人がいつも同じ椅子を引くようにする)。
/// </summary>
public void PullFor(string reason, int seatOrder)
{
if (spec == null || _aisleChairs.Count == 0) return;
int i = Mathf.Abs(seatOrder) % _aisleChairs.Count;
Pull(i, reason);
}
void Pull(int i, string reason)
{
if (i < 0 || i >= _aisleChairs.Count) return;
Chair c = _aisleChairs[i];
if (c == null) return;
// すでに引いてあるなら、引いたままの時間だけ延ばす(二重に数えない)
if (_holdLeft[i] > 0f)
{
_holdLeft[i] = Mathf.Max(_holdLeft[i], spec.chairPullHoldSec);
return;
}
c.Pull(spec.chairPullDistance);
_holdLeft[i] = spec.chairPullHoldSec;
_pulledNow++;
_pulls++;
if (_ops != null) _ops.AddChairPull(reason, TableIdOf());
}
int TableIdOf()
{
if (_ops == null || _tableIndex < 0 || _tableIndex >= _ops.Tables.Count) return 0;
return _ops.Tables[_tableIndex].tableId;
}
float NextIdleInterval()
{
if (spec == null || spec.chairPullIdleMeanIntervalSec <= 0f) return float.MaxValue;
double u = _rng.NextDouble();
if (u <= 1e-9) u = 1e-9;
return (float)(-spec.chairPullIdleMeanIntervalSec * System.Math.Log(u));
}
void Update()
{
if (spec == null || _rng == null) return;
float dt = Time.deltaTime;
// 引いたままの時間が切れた椅子を戻す
for (int i = 0; i < _holdLeft.Count; i++)
{
if (_holdLeft[i] <= 0f) continue;
_holdLeft[i] -= dt;
if (_holdLeft[i] > 0f) continue;
_holdLeft[i] = 0f;
if (_aisleChairs[i] != null) _aisleChairs[i].Release();
_pulledNow = Mathf.Max(0, _pulledNow - 1);
}
// 食事中に何もなく引く分(少しだけ)
if (_nextIdlePullIn == float.MaxValue) return;
_nextIdlePullIn -= dt;
if (_nextIdlePullIn > 0f) return;
_nextIdlePullIn = NextIdleInterval();
if (!Occupied()) return;
if (_aisleChairs.Count == 0) return;
Pull(_rng.Next(_aisleChairs.Count), ReasonIdle);
}
/// <summary>この卓に組が座っているか</summary>
bool Occupied()
{
if (_ops == null || _tableIndex < 0) return true; // 分からないときは引く
if (_tableIndex >= _ops.Tables.Count) return true;
return _ops.Tables[_tableIndex].groupId >= 0;
}
void OnDisable()
{
for (int i = 0; i < _holdLeft.Count; i++)
{
_holdLeft[i] = 0f;
if (_aisleChairs[i] != null) _aisleChairs[i].Release();
}
_pulledNow = 0;
}
}
}
People/StaffAgent.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// スタッフ1人。確率で仕事を選ぶのではなく、そのときに起きている仕事を
/// 仕事板(ServiceOps)から優先度の順に取って片付ける。
/// 優先1:案内・料理を運ぶ・呼び出し・会計 / 優先2:片付け
/// 手が空いているときはパントリーの前で待つ。
///
/// この script は人だけを扱う。どの仕事が要るかは ServiceOps が決める。
/// 数字は ServiceSpec と PeopleSpec の .asset に置く。
/// </summary>
[RequireComponent(typeof(NavMeshAgent))]
public class StaffAgent : MonoBehaviour
{
/// <summary>行き先1つぶん</summary>
struct Step
{
public Vector3 point;
public float dwellSec;
public bool handDish; // ここで料理が届く
}
[Header("設定")]
public PeopleSpec spec;
public ServiceSpec service;
[Tooltip("仕事板")]
public ServiceOps ops;
[Header("いまの様子(読むだけ)")]
[SerializeField] string _jobText = "待機";
[SerializeField] int _step;
[SerializeField] float _stepS; // いまの行き先へ歩き始めてからの秒数
NavMeshAgent _agent;
PersonRobotYield _yield; // ロボットをよけている最中は「着いた」と見ない
PeopleTargets _targets;
StaffJob _job;
readonly List<Step> _steps = new List<Step>();
float _timer;
bool _dwelling;
bool _idleParked;
Vector3 _pending;
bool _needsDestination;
/// <summary>同じ仕事を何回あきらめたか(仕事ごとに数える)</summary>
readonly Dictionary<StaffJob, int> _tries = new Dictionary<StaffJob, int>();
/// <summary>いまの仕事(頭の上の字幕が読む)</summary>
public StaffJob CurrentJob { get { return _job; } }
public string JobText { get { return _jobText; } }
public int Step_ { get { return _step; } }
/// <summary>いま向かっている点(頭の上の字幕が読む)</summary>
public Vector3 TargetPoint
{
get { return _step >= 0 && _step < _steps.Count ? _steps[_step].point : transform.position; }
}
void Awake()
{
_agent = GetComponent<NavMeshAgent>();
_yield = GetComponent<PersonRobotYield>();
}
/// <summary>ロボットをよけている最中か(行き先がよけ場所にすり替わっている)</summary>
bool Yielding
{
get
{
if (_yield == null) _yield = GetComponent<PersonRobotYield>();
return _yield != null && _yield.Intervening;
}
}
/// <summary>何人目のスタッフか(待つ点を1人に1つ割り当てるのに使う)</summary>
public int staffIndex;
/// <summary>手の空いたときに待つ点。無ければパントリーの前</summary>
Vector3 StandbyPoint
{
get
{
if (_targets == null) return transform.position;
int n = _targets.staffStandby.Count;
if (n == 0) return _targets.pantry;
return _targets.staffStandby[((staffIndex % n) + n) % n];
}
}
public void Configure(PeopleSpec peopleSpec, ServiceSpec serviceSpec, ServiceOps serviceOps,
PeopleTargets targets, int seed, int index)
{
staffIndex = index;
spec = peopleSpec;
service = serviceSpec;
ops = serviceOps;
_targets = targets;
if (_agent == null) _agent = GetComponent<NavMeshAgent>();
ApplySpec();
_job = null;
_steps.Clear();
_step = 0;
_stepS = 0f;
_dwelling = false;
_idleParked = false;
_tries.Clear();
_jobText = "待機";
}
void ApplySpec()
{
if (_agent == null || spec == null) return;
_agent.radius = spec.personRadius;
_agent.height = spec.personHeight;
_agent.speed = spec.staffSpeed;
_agent.acceleration = spec.acceleration;
_agent.angularSpeed = spec.angularSpeedDegPerSec;
_agent.stoppingDistance = spec.stoppingDistance;
_agent.obstacleAvoidanceType = spec.avoidanceQuality;
_agent.avoidancePriority = spec.avoidancePriority;
_agent.autoRepath = true;
// 人は人用の NavMesh(半径 0.2 m)を歩く。ロボットは 0.25 m のまま
PeopleNav.ApplyAgentType(_agent);
// スタッフはレジの内側(店員側)に入れる
_agent.areaMask = NavMesh.AllAreas;
}
void Update()
{
if (_agent == null || spec == null || service == null || ops == null || _targets == null) return;
// 行き先に着けないまま時間が過ぎたら、仕事を板に戻す(B1)。
// NavMesh に乗れないときも時間が進むように、下で戻る前に数える
if (_job != null && !_dwelling)
{
_stepS += Time.deltaTime;
if (GiveUpIfLate()) return;
}
// 時間の使い方を残す(案内・料理・呼び出し・会計・片付け・パントリーで待つ)
ops.AddStaffTime(_job != null ? JobKindName(_job.kind) : "待機", Time.deltaTime);
if (!PeopleNav.EnsureReady(transform, _agent)) return;
if (_needsDestination && !PeopleNav.Go(_agent, _pending)) return;
_needsDestination = false;
if (_job == null) { Idle(); return; }
if (_dwelling)
{
_timer += Time.deltaTime;
if (_timer < _steps[_step].dwellSec) return;
_dwelling = false;
Deliver(_steps[_step]);
_step++;
if (_step >= _steps.Count) { Done(); return; }
Go(_steps[_step].point);
return;
}
// ロボットをよけている最中は、行き先がよけ場所にすり替わっているので
// 「着いた」と見てはいけない
if (Yielding) return;
if (_agent.pathPending) return;
if (_agent.remainingDistance > _agent.stoppingDistance + 0.05f) return;
if (_steps[_step].dwellSec > 0f) { _dwelling = true; _timer = 0f; return; }
Deliver(_steps[_step]);
_step++;
if (_step >= _steps.Count) { Done(); return; }
Go(_steps[_step].point);
}
/// <summary>手が空いているとき。次の仕事を取り、無ければパントリーの前で待つ</summary>
void Idle()
{
StaffJob job = ops.TakeNextJob();
if (job == null)
{
if (!_idleParked)
{
_idleParked = true;
_jobText = "待機";
Go(StandbyPoint);
}
return;
}
_idleParked = false;
Begin(job);
}
/// <summary>仕事の名前(時間の使い方の記録に使う)</summary>
public static string JobKindName(StaffJobKind kind)
{
switch (kind)
{
case StaffJobKind.Guide: return "案内";
case StaffJobKind.Serve: return "料理";
case StaffJobKind.Call: return "呼び出し";
case StaffJobKind.Pay: return "会計";
case StaffJobKind.Clear: return "片付け";
}
return "そのほか";
}
void Begin(StaffJob job)
{
_job = job;
_steps.Clear();
_step = 0;
_dwelling = false;
_timer = 0f;
switch (job.kind)
{
case StaffJobKind.Guide:
_jobText = "案内";
Add(_targets.waiting, service.guideSec, false);
Add(TablePoint(job), 0f, false);
break;
case StaffJobKind.Serve:
_jobText = "料理を運ぶ";
Add(_targets.foodPass, 0f, false);
Add(TablePoint(job), service.staffPlaceSec, true);
Add(_targets.foodPass, 0f, false);
break;
case StaffJobKind.Call:
_jobText = "呼び出し";
Add(TablePoint(job), service.callSec, false);
break;
case StaffJobKind.Pay:
// スタッフはレジの内側(客と反対側。太い通路からだけ入れる)に立つ
_jobText = "会計";
Add(_targets.registerBack, service.paySec, false);
break;
case StaffJobKind.Clear:
// 皿をまとめ、返却口へ運び、戻って拭いて、もう一度運ぶ(bus_trips ぶん)
_jobText = "片付け";
Add(TablePoint(job), service.clearSec, false);
int trips = Mathf.Max(1, service.busTrips);
for (int i = 0; i < trips; i++)
{
Add(_targets.dishReturn, 0f, false);
if (i < trips - 1) Add(TablePoint(job), 0f, false);
}
Add(TablePoint(job), service.wipeSec, false);
break;
}
if (_steps.Count == 0) { Done(); return; }
Go(_steps[0].point);
}
Vector3 TablePoint(StaffJob job)
{
if (ops == null || job.tableIndex < 0 || job.tableIndex >= ops.Tables.Count)
return _targets.foodPass;
return ops.Tables[job.tableIndex].deliveryPoint;
}
void Add(Vector3 point, float dwellSec, bool handDish)
{
Step s = new Step();
s.point = point;
s.dwellSec = dwellSec;
s.handDish = handDish;
_steps.Add(s);
}
bool _wasInRegisterBack;
/// <summary>
/// 会計でないスタッフが店員側(レジの内側)に入っていないかを見る。
/// 店員側は1人ぶんなので、会計の仕事を持っていないスタッフは入らない。
/// </summary>
void WatchRegisterBack()
{
if (ops == null || ops.store == null) { _wasInRegisterBack = false; return; }
PlanRect back = ops.store.RegisterBackRect;
bool paying = _job != null && _job.kind == StaffJobKind.Pay;
bool inside = !back.IsEmpty && back.Contains(transform.position);
if (inside && !paying)
{
ops.AddRegisterBackIntruder(Time.deltaTime, !_wasInRegisterBack);
_wasInRegisterBack = true;
return;
}
_wasInRegisterBack = inside && !paying;
}
/// <summary>
/// 会計を済ませたとき、店員側(レジの内側)に立っていたかを残す。
/// 0 にならなければ、スタッフが客側に立ってしまっている。
/// </summary>
void RecordPayPlace()
{
if (ops == null || ops.store == null || _targets == null) return;
PlanRect back = ops.store.RegisterBackRect;
bool atBack = !back.IsEmpty && back.Expand(0.1f).Contains(transform.position);
// 立つ点と、実際に立った位置の距離
Vector3 a = transform.position; a.y = 0f;
Vector3 b = _targets.registerBack; b.y = 0f;
ops.AddRegisterPay(atBack, Vector3.Distance(a, b));
}
void Deliver(Step s)
{
// 会計を済ませた瞬間に、店員側に立っていたかを残す
if (_job != null && _job.kind == StaffJobKind.Pay) RecordPayPlace();
if (!s.handDish || _job == null || _job.dish == null) return;
ops.DishArrived(_job.dish, false);
}
void Done()
{
StaffJob job = _job;
_job = null;
_steps.Clear();
_step = 0;
_stepS = 0f;
_jobText = "待機";
if (job != null) { _tries.Remove(job); ops.FinishJob(job); }
}
void Go(Vector3 point)
{
_pending = point;
_needsDestination = true;
_stepS = 0f;
if (PeopleNav.EnsureReady(transform, _agent) && PeopleNav.Go(_agent, point))
_needsDestination = false;
}
// ---------------- あきらめて、やり直す(B1)----------------
/// <summary>
/// 行き先に着けないまま spec.staffGiveUpSec が過ぎたとき。
/// ・まだ staffGiveUpMaxTries 回未満なら、仕事を板に戻して別の仕事を取る
/// ・その回数に達したら、その場所へ移して仕事を続ける(店が通れないしるし)
/// </summary>
bool GiveUpIfLate()
{
if (spec.staffGiveUpSec <= 0f || _stepS < spec.staffGiveUpSec) return false;
StaffJob job = _job;
Vector3 to = _step >= 0 && _step < _steps.Count ? _steps[_step].point : transform.position;
int tries;
if (!_tries.TryGetValue(job, out tries)) tries = 0;
tries++;
_tries[job] = tries;
int max = Mathf.Max(1, spec.staffGiveUpMaxTries);
if (tries >= max)
{
// 何度やっても着けない。その場所へ移して仕事を済ませる
ops.AddGiveUp("スタッフ", _jobText, job != null ? job.tableId : 0,
to, transform.position, _stepS, "移した");
_tries.Remove(job);
PlaceAt(to);
_stepS = 0f;
return false; // 移したので、このコマの続きで着いた扱いにする
}
// 仕事を板に戻して、別の仕事を取る
ops.AddGiveUp("スタッフ", _jobText, job != null ? job.tableId : 0,
to, transform.position, _stepS, "あきらめた");
_job = null;
_steps.Clear();
_step = 0;
_dwelling = false;
_stepS = 0f;
_jobText = "待機";
ops.AbandonJob(job);
return true;
}
/// <summary>体をその場所に移す(どうしても着けないとき)</summary>
void PlaceAt(Vector3 p)
{
transform.position = p;
if (Application.isPlaying && _agent != null && _agent.enabled && _agent.isOnNavMesh)
_agent.Warp(p);
}
}
}
People/WaitingGuest.cs
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// 待合に立つ客1人。待合の中で立ち止まっていて、ときどき少し位置を変えるだけ。
/// 人数は混み具合で変える(PeopleSpec)。
///
/// この script は人だけを扱う。
/// </summary>
[RequireComponent(typeof(NavMeshAgent))]
public class WaitingGuest : MonoBehaviour
{
[Header("設定")]
public PeopleSpec spec;
NavMeshAgent _agent;
PeopleTargets _targets;
System.Random _rng;
float _timer;
Vector3 _pendingDestination;
bool _needsDestination;
void Awake()
{
_agent = GetComponent<NavMeshAgent>();
}
public void Configure(PeopleSpec peopleSpec, PeopleTargets targets, int seed)
{
spec = peopleSpec;
_targets = targets;
_rng = new System.Random(seed);
if (_agent == null) _agent = GetComponent<NavMeshAgent>();
ApplySpec();
_timer = (float)_rng.NextDouble() * spec.waitingShuffleSec;
_needsDestination = false;
}
void ApplySpec()
{
if (_agent == null || spec == null) return;
_agent.radius = spec.personRadius;
_agent.height = spec.personHeight;
_agent.speed = spec.guestSpeed;
_agent.acceleration = spec.acceleration;
_agent.angularSpeed = spec.angularSpeedDegPerSec;
_agent.stoppingDistance = spec.stoppingDistance;
_agent.obstacleAvoidanceType = spec.avoidanceQuality;
_agent.avoidancePriority = spec.avoidancePriority;
_agent.autoRepath = true;
}
void Update()
{
if (_agent == null || spec == null || _rng == null || _targets == null) return;
if (!PeopleNav.EnsureReady(transform, _agent)) return;
if (_needsDestination && !PeopleNav.Go(_agent, _pendingDestination)) return;
_needsDestination = false;
_timer -= Time.deltaTime;
if (_timer > 0f) return;
_timer = spec.waitingShuffleSec;
_pendingDestination = _targets.RandomWaitingSpot(_rng);
_needsDestination = true;
if (PeopleNav.Go(_agent, _pendingDestination)) _needsDestination = false;
}
}
}
Service/GroupManager.cs
using System.Collections.Generic;
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 客を「1組ごとに、入ってから出るまで」で動かす(試算の group() と同じ流れ)。
///
/// 入店 → 卓が空くまで待合(あふれたら店の外) → 案内 → 着席 → 料理 → 食事
/// (食事中にドリンクバー・トイレ・呼び出し) → 会計 → 退店 → 片付け
///
/// 座っている人は人として置かない(通路に出ないため)。
/// 立ち上がった人だけを、使い回しの体(Guest.prefab)で歩かせる。
///
/// この script は人だけを扱う。卓の空きと料理の待ち行列は ServiceOps に任せる。
/// 数字は ServiceSpec と PeopleSpec の .asset に置く。
/// </summary>
public class GroupManager : MonoBehaviour
{
/// <summary>1組の今の状態</summary>
public enum GState { Waiting, Guided, Seated, Eating, Paying, Left, Clearing, Done }
/// <summary>1組</summary>
public class Group
{
public int id;
public int size;
public GState state = GState.Waiting;
public TableSlot table;
public float arriveS = -1f;
public float guideStartS = -1f;
public float seatedS = -1f;
public float eatEndS = -1f;
public float payS = -1f;
public float leftS = -1f;
public bool visibleWaiting;
public bool warmup; // 慣らしのために最初から座っている組
public readonly List<DishOrder> dishes = new List<DishOrder>();
public readonly List<float> cookAt = new List<float>(); // 受け渡し口に出す時刻
public readonly List<bool> cookFragile = new List<bool>();
public readonly List<float> errandAt = new List<float>(); // 立ち上がる時刻
public readonly List<int> errandKind = new List<int>(); // 0=ドリンクバー 1=トイレ
public readonly List<float> callAt = new List<float>();
public int activeErrands;
public int dishesOrdered;
/// <summary>何人目が立ったか。いつも同じ人が同じ椅子を引くようにする(B6)</summary>
public int chairTurn;
/// <summary>会計・片付けで止まったままの秒数(B1。長びいたら卓を空ける)</summary>
public float stuckS;
public GuestAgent waitingBody; // 待合に立たせている1人
// ---- 会計(レジに立つのは組の1人だけ)----
/// <summary>会計に立つ1人の体。ほかの人は席に座ったまま</summary>
public GuestAgent payerBody;
/// <summary>取ったレジの並ぶ点。-1 なら取れていない(席で待つ)</summary>
public int payerSpot = -1;
/// <summary>会計の人をレジへ送ったか</summary>
public bool payerSent;
/// <summary>会計を始めた時刻(食べ終わって Paying に入った時刻)</summary>
public float payStartS = -1f;
public StaffJob job; // いま出している仕事
}
[Header("設定")]
public ServiceSpec spec;
public PeopleSpec people;
public ServiceOps ops;
public StoreBuilder store;
[Tooltip("卓ごとの椅子の出し入れを読む先(B6)。無くても動く")]
public PeopleSpawner spawner;
[Header("部品")]
[Tooltip("歩く客の体。待合の客にも使い回す")]
public GameObject guestPrefab;
[Tooltip("使っていない(段階4の場面の再現用に残してある)")]
public GameObject waitingGuestPrefab;
[Tooltip("歩く人を置く親")]
public Transform bodyRoot;
[Header("いまの様子(読むだけ)")]
[SerializeField] int _groupsArrived;
[SerializeField] int _groupsSeated;
[SerializeField] int _groupsDone;
[SerializeField] int _waitingGroups;
[SerializeField] int _walkingBodies;
readonly List<Group> _groups = new List<Group>();
readonly List<Group> _waiting = new List<Group>();
readonly List<GuestAgent> _pool = new List<GuestAgent>();
readonly List<GuestAgent> _live = new List<GuestAgent>();
PeopleTargets _targets;
System.Random _rng;
float _nextArrivalS;
int _nextGroupId = 1;
CrowdLevel _crowd;
public IList<Group> Groups { get { return _groups; } }
public int GroupsArrived { get { return _groupsArrived; } }
public int GroupsSeated { get { return _groupsSeated; } }
public int GroupsDone { get { return _groupsDone; } }
public int WaitingGroups { get { return _waitingGroups; } }
/// <summary>いま通路を歩いている人の数(スタッフを除く)</summary>
public int WalkingBodies { get { return _walkingBodies; } }
// ---------------- 用意 ----------------
public void Setup(CrowdLevel crowd, int seed, PeopleTargets targets)
{
_crowd = crowd;
_rng = new System.Random(seed * 7919 + 13);
_targets = targets;
_groups.Clear();
_waiting.Clear();
ReturnAllBodies();
_groupsArrived = 0;
_groupsSeated = 0;
_groupsDone = 0;
_nextGroupId = 1;
float rate = spec.ArrivalsPerHour(crowd) / 3600f;
_nextArrivalS = ServiceSpec.ExponentialSec(_rng, rate);
SeedWarmupGroups();
}
/// <summary>
/// 慣らしのために、卓の一部を「食事の途中の組」で埋めて始める。
/// 空の店から始めると、はじめの数十分が本番と違う形になるため。
/// </summary>
void SeedWarmupGroups()
{
if (people == null || ops == null) return;
int tables = ops.Tables.Count;
int want = Mathf.RoundToInt(tables * Mathf.Clamp01(people.warmupOccupancy));
int want0 = want;
int placed = 0;
for (int i = 0; i < tables && want > 0; i++)
{
TableSlot slot = ops.TakeTableAt(i, _nextGroupId);
if (slot == null) continue;
Group g = new Group();
g.id = _nextGroupId++;
// 2人席に3人以上を座らせない。卓の席数で頭を切る
g.size = Mathf.Min(spec.PickGroupSize(_rng), Mathf.Max(1, slot.seatCount));
g.table = slot;
g.warmup = true;
g.arriveS = 0f;
g.seatedS = 0f;
// 食事のどの時点かを決める(B3)。
// 0.00〜0.25:座ったばかりで料理を待っている(1皿めも2皿めもまだ)
// 0.25〜0.50:1皿めは届いたが2皿めを待っている
// 0.50〜1.00:受け取って食べている〜食べ終わりかけ
// でたらめにすると食べ終わりかけが固まり、そのぶんの会計がいちどに来て
// レジが詰まるので、均等にばらす
double phase = (placed + 0.5) / Mathf.Max(1, want0);
if (phase < 0.25) SeedWaitingFirstDishes(g, 2);
else if (phase < 0.5) SeedWaitingFirstDishes(g, 1);
else SeedEating(g, (float)((phase - 0.5) / 0.5));
placed++;
_groups.Add(g);
_groupsArrived++;
_groupsSeated++;
want--;
}
}
/// <summary>
/// 慣らし:座ったばかりで料理を待っている組(B3)。
/// まだ届いていない皿は、作る時間の残りをでたらめに決めて受け渡し口の待ち行列に入る。
/// </summary>
void SeedWaitingFirstDishes(Group g, int pending)
{
g.state = GState.Seated;
g.dishesOrdered = 2;
// 1皿め・2皿めの作る時間。pending が 1 なら1皿めは届いた扱いにする
float cook1 = ServiceSpec.TriangularSec(_rng, spec.firstDish1CookMin);
float cook2 = ServiceSpec.TriangularSec(_rng, spec.firstDish2CookMin);
if (pending >= 2)
{
g.cookAt.Add((float)_rng.NextDouble() * cook1);
g.cookFragile.Add(false);
}
else
{
// 1皿めは届いている
g.dishes.Add(ops.SeedArrivedDish(g.id, g.table));
}
g.cookAt.Add((float)_rng.NextDouble() * cook2);
g.cookFragile.Add(false);
}
/// <summary>
/// 慣らし:もう食べている組(B3)。
/// done は食事の進み具合(0 なら食べ始め、1 なら食べ終わりかけ)。
/// 均等にばらすので、食べ終わりがいちどに来ない。
/// </summary>
void SeedEating(Group g, float done)
{
g.state = GState.Eating;
g.dishesOrdered = 2;
// 最初の料理は届いている扱い
for (int k = 0; k < 2; k++) g.dishes.Add(ops.SeedArrivedDish(g.id, g.table));
float eat = ServiceSpec.TriangularSec(_rng, spec.eatMin);
g.eatEndS = eat * (1f - Mathf.Clamp01(done));
ScheduleDuringMeal(g, g.eatEndS);
}
// ---------------- 毎コマ ----------------
public void Tick(float dt)
{
if (spec == null || ops == null || _rng == null) return;
float now = ops.NowS;
Arrive(now);
SeatWaiting(now);
for (int i = 0; i < _groups.Count; i++) Advance(_groups[i], now, dt);
_waitingGroups = _waiting.Count;
_walkingBodies = _live.Count;
}
/// <summary>来客(指数分布。試算の arrivals() と同じ)</summary>
void Arrive(float now)
{
if (now < _nextArrivalS) return;
float rate = spec.ArrivalsPerHour(_crowd) / 3600f;
_nextArrivalS = now + ServiceSpec.ExponentialSec(_rng, rate);
Group g = new Group();
g.id = _nextGroupId++;
g.size = spec.PickGroupSize(_rng);
g.arriveS = now;
g.state = GState.Waiting;
_groups.Add(g);
_waiting.Add(g);
_groupsArrived++;
// 待合に立たせるのは上限まで。あふれた組は店の外で待つ扱い(人として置かない)
if (CountVisibleWaiting() < spec.waitingVisibleMax)
{
g.visibleWaiting = true;
g.waitingBody = BorrowBody(_targets.RandomWaitingSpot(_rng), true);
if (g.waitingBody != null)
g.waitingBody.Begin(people, GuestAgent.Errand.Waiting, g.waitingBody.transform.position, 0f,
false, Vector3.zero, null);
}
}
int CountVisibleWaiting()
{
int n = 0;
for (int i = 0; i < _waiting.Count; i++) if (_waiting[i].visibleWaiting) n++;
return n;
}
/// <summary>
/// 空いた卓に、待っている組を先着順で入れる。
/// 2人席と4人席があるので、先頭の組が入れる卓が無いときは、
/// その組を飛ばして次の組を見る(4人の組が先頭にいるあいだ、
/// 2人席が空いているのに誰も座れない、を避ける)。
/// </summary>
void SeatWaiting(float now)
{
while (_waiting.Count > 0 && ops.FreeTableCount > 0)
{
int pick = -1;
for (int i = 0; i < _waiting.Count; i++)
if (ops.HasFreeTableFor(_waiting[i].size)) { pick = i; break; }
if (pick < 0) break;
Group g = _waiting[pick];
TableSlot slot = ops.TakeFreeTable(g.id, g.size);
if (slot == null) break;
_waiting.RemoveAt(pick);
g.table = slot;
g.guideStartS = now;
g.state = GState.Guided;
ops.RecordSeatWait(g.arriveS);
// スタッフが待合へ迎えに行き、卓まで案内する
Group captured = g;
StaffJob job = ops.PostJob(StaffJobKind.Guide, g.id, slot, null, 1);
job.onDone = delegate { OnGuided(captured); };
g.job = job;
}
}
/// <summary>
/// その組の卓の椅子を1脚引く(B6)。人が動く瞬間だけ呼ぶ。
/// 何人目かを順に進めて、同じ人がいつも同じ椅子を引くようにする。
/// </summary>
void PullChair(Group g, string reason)
{
if (spawner == null || g == null || g.table == null) return;
SeatedGuest s = spawner.SeatedAt(g.table.index);
if (s == null) return;
s.PullFor(reason, g.chairTurn++);
}
void OnGuided(Group g)
{
g.job = null;
g.state = GState.Seated;
g.seatedS = ops.NowS;
// 席に着くので椅子を引く(B6)
PullChair(g, SeatedGuest.ReasonSit);
// 案内された組は席に着く(待合の体を返す)
if (g.waitingBody != null) { ReturnBody(g.waitingBody); g.waitingBody = null; }
g.visibleWaiting = false;
// 最初の注文(試算の first_dishes と同じ数・同じ作る時間)
g.cookAt.Add(g.seatedS + ServiceSpec.TriangularSec(_rng, spec.firstDish1CookMin));
g.cookFragile.Add(false);
g.cookAt.Add(g.seatedS + ServiceSpec.TriangularSec(_rng, spec.firstDish2CookMin));
g.cookFragile.Add(false);
g.dishesOrdered = 2;
}
/// <summary>食事中に起きること(デザート・呼び出し・ドリンクバー・トイレ)を先に決める</summary>
void ScheduleDuringMeal(Group g, float eatEndS)
{
float now = ops.NowS;
float eat = Mathf.Max(1f, eatEndS - now);
int nFragile = ServiceSpec.PickCount(_rng, spec.fragilePerGroup);
for (int i = 0; i < nFragile; i++)
{
float at = now + (0.3f + 0.55f * (float)_rng.NextDouble()) * eat;
g.cookAt.Add(at + ServiceSpec.TriangularSec(_rng, spec.fragileCookMin));
g.cookFragile.Add(true);
g.dishesOrdered++;
}
// 呼び出し(試算は2回ぶん call_rate/2 で判定)
for (int i = 0; i < 2; i++)
if (_rng.NextDouble() < spec.callRate / 2f)
g.callAt.Add(now + (float)_rng.NextDouble() * eat);
// ドリンクバー(1人あたり)とトイレ(1組あたり)
int drinks = ServiceSpec.PickCount(_rng, spec.drinkTripsPerPerson * g.size);
for (int i = 0; i < drinks; i++)
{
g.errandAt.Add(now + (float)_rng.NextDouble() * eat);
g.errandKind.Add(0);
}
int toilets = ServiceSpec.PickCount(_rng, spec.toiletTripsPerGroup);
for (int i = 0; i < toilets; i++)
{
g.errandAt.Add(now + (float)_rng.NextDouble() * eat);
g.errandKind.Add(1);
}
}
void Advance(Group g, float now, float dt)
{
// できた料理を受け渡し口に出す
for (int i = g.cookAt.Count - 1; i >= 0; i--)
{
if (now < g.cookAt[i]) continue;
if (g.table != null) g.dishes.Add(ops.PutDish(g.id, g.table, g.cookFragile[i]));
g.cookAt.RemoveAt(i);
g.cookFragile.RemoveAt(i);
}
// 呼び出し
for (int i = g.callAt.Count - 1; i >= 0; i--)
{
if (now < g.callAt[i]) continue;
if (g.table != null) ops.PostJob(StaffJobKind.Call, g.id, g.table, null, 1);
g.callAt.RemoveAt(i);
}
// 立ち上がってドリンクバー・トイレへ
for (int i = g.errandAt.Count - 1; i >= 0; i--)
{
if (now < g.errandAt[i]) continue;
StartErrand(g, g.errandKind[i]);
g.errandAt.RemoveAt(i);
g.errandKind.RemoveAt(i);
}
switch (g.state)
{
case GState.Seated:
// 最初の料理が全部届いたら食事が始まる(試算の AllOf(firsts) と同じ)
if (AllFirstDishesArrived(g))
{
g.state = GState.Eating;
g.eatEndS = now + ServiceSpec.TriangularSec(_rng, spec.eatMin);
ScheduleDuringMeal(g, g.eatEndS);
}
break;
case GState.Eating:
if (now < g.eatEndS) break;
if (g.cookAt.Count > 0 || !AllDishesArrived(g) || g.activeErrands > 0) break;
g.state = GState.Paying;
g.stuckS = 0f;
g.payStartS = now;
g.payerSent = false;
g.payerSpot = -1;
break;
case GState.Paying:
// 会計に立つのは組の1人だけ。レジの並ぶ点が空くまでは席で待つ
SendPayerIfPossible(g, now);
break;
case GState.Left:
// 退店したら、スタッフが片付けに行く(優先はいちばん後)
g.state = GState.Clearing;
g.stuckS = 0f;
if (g.table != null) g.table.clearing = true;
{
Group captured = g;
StaffJob job = ops.PostJob(StaffJobKind.Clear, g.id, g.table, null, 2);
job.onDone = delegate { OnCleared(captured); };
g.job = job;
}
break;
}
// 会計・片付けで止まって卓が空かないままなら、時間を見て卓を空ける(B1)
if (g.state == GState.Paying || g.state == GState.Clearing)
{
g.stuckS += dt;
if (spec.tableStuckFreeSec > 0f && g.stuckS >= spec.tableStuckFreeSec)
ForceFreeTable(g);
}
}
/// <summary>
/// 会計・片付けが進まないまま時間が過ぎた組の卓を、強引に空ける(B1)。
/// これが多いときは、人が通れない店になっているしるし。
/// </summary>
void ForceFreeTable(Group g)
{
Vector3 at = g.table != null ? g.table.deliveryPoint : Vector3.zero;
ops.AddGiveUp("組", g.state == GState.Paying ? "会計" : "片付け",
g.table != null ? g.table.tableId : 0,
at, at, g.stuckS, "卓を空けた");
// 出していた仕事を板から外す
if (g.job != null) { ops.FinishJobSilently(g.job); g.job = null; }
ReleasePayer(g);
if (g.leftS < 0f) g.leftS = ops.NowS;
ops.ReleaseTable(g.table, g.leftS);
g.table = null;
g.state = GState.Done;
g.stuckS = 0f;
_groupsDone++;
}
/// <summary>
/// 会計に立つ1人をレジへ送る。
/// ・レジの並ぶ点が空いていなければ、席に座ったまま待つ(レジに団子にならないように)
/// ・送れたときだけ、スタッフの会計の仕事を板に出す(誰も居ないレジで会計しないように)
/// 残りの人は会計が終わってから、全員で立って店を出る(OnLeft)。
/// </summary>
void SendPayerIfPossible(Group g, float now)
{
if (g.payerSent) return;
// 前のとおり(誰もレジに立たず、スタッフだけで会計)に戻せるようにしておく。
// レジの前の人数を直す前・直したあとで比べるため
if (!spec.payAtRegisterByOne)
{
g.payerSent = true;
PullChair(g, SeatedGuest.ReasonPay);
Group old = g;
StaffJob j = ops.PostJob(StaffJobKind.Pay, g.id, g.table, null, 1);
j.onDone = delegate { OnPaid(old); };
g.job = j;
return;
}
Vector3 spot;
int index = ops.TakeRegisterQueueSpot(out spot);
if (index < 0) return; // 並ぶ点が埋まっている。席で待つ
g.payerSpot = index;
g.payerSent = true;
// 会計で立ち上がるので椅子を引く(B6)
PullChair(g, SeatedGuest.ReasonPay);
Vector3 from = g.table != null ? g.table.deliveryPoint : _targets.entrance;
g.payerBody = BorrowBody(from, false);
if (g.payerBody != null)
{
g.payerBody.SetTable(g.table != null ? g.table.tableId : 0);
g.payerBody.Begin(people, GuestAgent.Errand.Pay, spot, float.MaxValue,
false, from, null);
}
Group captured = g;
StaffJob job = ops.PostJob(StaffJobKind.Pay, g.id, g.table, null, 1);
job.onDone = delegate { OnPaid(captured); };
g.job = job;
}
/// <summary>会計の人を席へ戻し、レジの並ぶ点を空ける</summary>
void ReleasePayer(Group g)
{
if (g.payerBody != null) { ReturnBody(g.payerBody); g.payerBody = null; }
if (g.payerSpot >= 0) { ops.ReleaseRegisterQueueSpot(g.payerSpot); g.payerSpot = -1; }
g.payerSent = false;
}
void OnPaid(Group g)
{
ReleasePayer(g);
g.job = null;
g.payS = ops.NowS;
g.leftS = ops.NowS;
g.state = GState.Left;
g.stuckS = 0f;
// 退店するので椅子を引く(B6)
PullChair(g, SeatedGuest.ReasonLeave);
// 組の全員が立って入口から出る
int tableId = g.table != null ? g.table.tableId : 0;
for (int i = 0; i < g.size; i++)
{
Vector3 from = g.table != null ? g.table.deliveryPoint : _targets.entrance;
GuestAgent a = BorrowBody(from, false);
if (a == null) break;
GuestAgent captured = a;
a.SetTable(tableId);
a.Begin(people, GuestAgent.Errand.Leave, _targets.entrance, 0f, false, Vector3.zero,
delegate { ReturnBody(captured); });
}
}
void OnCleared(Group g)
{
g.job = null;
ops.ReleaseTable(g.table, g.leftS);
g.state = GState.Done;
_groupsDone++;
}
void StartErrand(Group g, int kind)
{
if (g.table == null) return;
Vector3 home = g.table.deliveryPoint;
Vector3 target = kind == 1 ? _targets.toilet : _targets.drinkBar;
float dwell = kind == 1 ? spec.toiletSec : spec.drinkBarSec;
// ドリンクバーは、空いている機械の前へ行く。埋まっていたら端に並ぶ。
// それも埋まっていたら、立ち上がらずにあきらめる(B4)
int token = -1;
if (kind == 0 && ops.HasDrinkBarSpots)
{
Vector3 spot;
bool queued;
token = ops.TakeDrinkBarSpot(out spot, out queued);
if (token < 0)
{
ops.AddGiveUp("客", "ドリンクバー", g.table.tableId, target, home, 0f, "あきらめた");
return;
}
target = spot;
}
GuestAgent a = BorrowBody(home, false);
if (a == null) { ops.ReleaseDrinkBarSpot(token, 0f); return; }
// 立ち上がるので椅子を引く(B6)
PullChair(g, kind == 1 ? SeatedGuest.ReasonToilet : SeatedGuest.ReasonDrinkBar);
g.activeErrands++;
Group cg = g;
GuestAgent ca = a;
int ct = token;
float startS = ops.NowS;
a.SetTable(g.table.tableId);
a.Begin(people, kind == 1 ? GuestAgent.Errand.Toilet : GuestAgent.Errand.DrinkBar,
target, dwell, true, home,
delegate
{
cg.activeErrands--;
ops.ReleaseDrinkBarSpot(ct, ops.NowS - startS - dwell);
PullChair(cg, SeatedGuest.ReasonSitBack); // 戻って座り直す(B6)
ReturnBody(ca);
});
}
bool AllFirstDishesArrived(Group g)
{
if (g.cookAt.Count > 0) return false;
if (g.dishes.Count < 2) return false;
for (int i = 0; i < 2 && i < g.dishes.Count; i++)
if (g.dishes[i].arrivedS < 0f) return false;
return true;
}
bool AllDishesArrived(Group g)
{
for (int i = 0; i < g.dishes.Count; i++)
if (g.dishes[i].arrivedS < 0f) return false;
return true;
}
// ---------------- 歩く体の使い回し ----------------
GuestAgent BorrowBody(Vector3 at, bool waitingKind)
{
// 待合の客も同じ体を使う(GuestAgent が「待っている」用事を持っている)
GameObject prefab = guestPrefab;
if (prefab == null || bodyRoot == null) return null;
GuestAgent a = null;
for (int i = _pool.Count - 1; i >= 0; i--)
{
GuestAgent c = _pool[i];
_pool.RemoveAt(i);
if (c == null) continue;
a = c;
break;
}
if (a == null)
{
GameObject go = Instantiate(prefab, bodyRoot);
a = go.GetComponent<GuestAgent>();
if (a == null) { Destroy(go); return null; }
}
a.gameObject.SetActive(true);
a.ops = ops; // あきらめたことを残す先(B1)
// 人がロボットをよける仕掛け(prefab に無くても付ける)
if (spawner != null) spawner.AttachRobotYield(a.gameObject, _nextGroupId * 31 + _live.Count);
a.PlaceAt(at);
_live.Add(a);
return a;
}
void ReturnBody(GuestAgent a)
{
if (a == null) return;
_live.Remove(a);
a.Stop();
a.gameObject.SetActive(false);
_pool.Add(a);
}
void ReturnAllBodies()
{
for (int i = _live.Count - 1; i >= 0; i--) ReturnBody(_live[i]);
_live.Clear();
}
public void ClearAll()
{
ReturnAllBodies();
for (int i = 0; i < _pool.Count; i++)
if (_pool[i] != null) Destroy(_pool[i].gameObject);
_pool.Clear();
_groups.Clear();
_waiting.Clear();
}
}
}
Service/JamWatcher.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
namespace ServiceSim
{
/// <summary>
/// 人どうしのすれ違い。細い通路で押し合って動けなくなったときに解く。
/// 1. 前に進めない状態が 1 秒続いたら、その人の半径を細める(体を横に向ける)
/// 2. それでも 3 秒抜けられなければ、片方が下がる(仕事中のスタッフより客が下がる)
/// 3. 1回の詰まりを people_jams.csv に1行で残す
///
/// この script は人だけを扱う。数字は PeopleSpec の .asset に置く。
/// </summary>
public class JamWatcher : MonoBehaviour
{
/// <summary>詰まり1回ぶんの記録</summary>
public struct JamRow
{
public float tS; // 店の中の時刻
public float x, z;
public string aisle;
public int people; // 詰まっていた人数
public float durationS;
public string resolved; // どう解けたか
/// <summary>
/// 近くでロボットが止まっていたか(B9)。
/// 停止点を通路の中央にすると、通路 1.0 m では停まった台の左右が 0.25 m ずつになり、
/// 人(半径 0.2 m)はどちら側も通れない。その回数を数える。
/// </summary>
public bool robotBlocked;
/// <summary>その台の番号(robotBlocked のときだけ)</summary>
public int robotId;
}
[Header("設定")]
public PeopleSpec spec;
public ServiceOps ops;
public StoreBuilder store;
[Tooltip("この距離の中にいる動けない人を、同じ詰まりとして数える(m)")]
public float clusterRadius = 1.0f;
[Tooltip("この距離の中に止まっているロボットがいたら、そのロボットが原因とみなす(m)")]
public float robotNearM = 1.5f;
[Header("いまの様子(読むだけ)")]
[SerializeField] int _narrowed;
[SerializeField] int _jams;
[SerializeField] int _robotBlockedJams;
class State
{
public float stuckS;
public bool narrowed;
public bool backedOff;
public float startS;
public Vector3 startPos;
public int people;
public bool robotBlocked; // 詰まっている間に、近くで止まっているロボットがいた
public int robotId;
}
readonly Dictionary<PersonBody, State> _state = new Dictionary<PersonBody, State>();
readonly List<JamRow> _rows = new List<JamRow>();
public IReadOnlyList<JamRow> Rows { get { return _rows; } }
public int JamCount { get { return _jams; } }
/// <summary>人どうしの詰まりのうち、近くでロボットが止まっていたもの(B9)</summary>
public int RobotBlockedJamCount { get { return _robotBlockedJams; } }
public float TotalJamSeconds
{
get
{
float s = 0f;
for (int i = 0; i < _rows.Count; i++) s += _rows[i].durationS;
return s;
}
}
public void Setup()
{
_state.Clear();
_rows.Clear();
_jams = 0;
_narrowed = 0;
_robotBlockedJams = 0;
}
public void Tick(float dt)
{
if (spec == null || ops == null) return;
int narrowed = 0;
IReadOnlyList<PersonBody> all = PersonBody.All;
for (int i = 0; i < all.Count; i++)
{
PersonBody p = all[i];
if (p == null || !p.walksInAisle) continue;
NavMeshAgent a = p.GetComponent<NavMeshAgent>();
if (a == null || !a.isActiveAndEnabled || !a.isOnNavMesh) { Clear(p); continue; }
bool wantsToMove = a.hasPath && a.remainingDistance > a.stoppingDistance + 0.05f;
bool stuck = wantsToMove && p.Speed < spec.jamStuckSpeed;
if (!stuck) { Resolve(p, a, "そのまま抜けた"); continue; }
State st;
if (!_state.TryGetValue(p, out st))
{
st = new State();
st.startS = ops.NowS;
st.startPos = p.transform.position;
_state[p] = st;
}
st.stuckS += dt;
st.people = Mathf.Max(st.people, CountStuckNear(p));
// 近くで止まっているロボットがいたら覚えておく(B9)
if (!st.robotBlocked)
{
int id = StoppedRobotNear(p.transform.position);
if (id > 0) { st.robotBlocked = true; st.robotId = id; }
}
// 1. 体を横に向ける
if (!st.narrowed && st.stuckS >= spec.jamNarrowAfterSec)
{
st.narrowed = true;
a.radius = spec.jamNarrowRadius;
}
if (st.narrowed) narrowed++;
// 2. それでも抜けられなければ、片方が下がる
if (!st.backedOff && st.stuckS >= spec.jamBackOffAfterSec && ShouldBackOff(p))
{
st.backedOff = true;
Vector3 to = NearestWideAislePoint(p.transform.position);
PeopleNav.Go(a, to);
}
}
_narrowed = narrowed;
}
/// <summary>仕事中のスタッフより、客のほうが下がる</summary>
bool ShouldBackOff(PersonBody p)
{
if (p.kind != PersonKind.Staff) return true;
StaffAgent s = p.GetComponent<StaffAgent>();
return s == null || s.CurrentJob == null; // 手が空いているスタッフなら下がる
}
/// <summary>
/// その点の近くで止まっているロボットの番号。いなければ 0。
/// 「卓の前で止まっていて人が通れなかった」を数えるために使う(B9)。
/// </summary>
int StoppedRobotNear(Vector3 at)
{
if (ops == null) return 0;
IList<ServingRobotController> robots = ops.Robots;
for (int i = 0; i < robots.Count; i++)
{
ServingRobotController r = robots[i];
if (r == null) continue;
if (r.CurrentPhase == ServingRobotController.Phase.Idle ||
r.CurrentPhase == ServingRobotController.Phase.Done) continue;
if (r.LastStepM > 0.002f) continue; // 動いている台は数えない
Vector3 d = r.transform.position - at;
d.y = 0f;
if (d.magnitude <= robotNearM) return r.robotId;
}
return 0;
}
int CountStuckNear(PersonBody p)
{
int n = 1;
IReadOnlyList<PersonBody> all = PersonBody.All;
for (int i = 0; i < all.Count; i++)
{
PersonBody o = all[i];
if (o == null || o == p || !o.walksInAisle) continue;
if (o.Speed >= spec.jamStuckSpeed) continue;
Vector3 d = o.transform.position - p.transform.position;
d.y = 0f;
if (d.magnitude <= clusterRadius) n++;
}
return n;
}
void Resolve(PersonBody p, NavMeshAgent a, string how)
{
State st;
if (!_state.TryGetValue(p, out st)) return;
_state.Remove(p);
if (st.narrowed && a != null) a.radius = spec.personRadius;
// 細める前に抜けた短いものは、詰まりとして数えない
if (st.stuckS < spec.jamNarrowAfterSec) return;
JamRow r = new JamRow();
r.tS = st.startS;
r.x = st.startPos.x;
r.z = st.startPos.z;
r.aisle = AisleNameAt(st.startPos);
r.people = Mathf.Max(2, st.people);
r.durationS = st.stuckS;
r.resolved = st.backedOff ? "下がって抜けた" : (st.narrowed ? "体を横に向けて抜けた" : how);
r.robotBlocked = st.robotBlocked;
r.robotId = st.robotId;
_rows.Add(r);
_jams++;
if (st.robotBlocked) _robotBlockedJams++;
}
void Clear(PersonBody p)
{
State st;
if (!_state.TryGetValue(p, out st)) return;
_state.Remove(p);
}
string AisleNameAt(Vector3 p)
{
if (store == null) return "";
string wide = "";
IList<StoreBuilder.AisleInfo> aisles = store.Aisles;
for (int i = 0; i < aisles.Count; i++)
{
Bounds b = aisles[i].zone;
if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue;
if (aisles[i].narrow) return aisles[i].name;
if (wide.Length == 0) wide = aisles[i].name;
}
return wide;
}
/// <summary>いちばん近い太い通路(太い通路・横通路)の点</summary>
Vector3 NearestWideAislePoint(Vector3 from)
{
if (store == null) return from;
float best = float.MaxValue;
Vector3 pick = from;
IList<StoreBuilder.AisleInfo> aisles = store.Aisles;
for (int i = 0; i < aisles.Count; i++)
{
if (aisles[i].narrow) continue;
Vector3 c = aisles[i].zone.ClosestPoint(new Vector3(from.x, 0f, from.z));
c.y = 0f;
float d = Vector3.Distance(new Vector3(from.x, 0f, from.z), c);
if (d < best) { best = d; pick = c; }
}
return pick;
}
}
}
Service/ServiceOps.cs
using System.Collections.Generic;
using UnityEngine;
namespace ServiceSim
{
/// <summary>料理1皿</summary>
public class DishOrder
{
public int id;
public int groupId;
public int tableIndex; // StoreBuilder.Tables の番号
public int tableId; // 卓の番号(報告用)
public float readyS = -1f; // 受け渡し口に出た時刻(店の中, 秒)
public float arrivedS = -1f;// 卓に届いた時刻
/// <summary>はじめて誰かが持ち上げた時刻(受け渡し口で待った時間を出すため)</summary>
public float firstTakenS = -1f;
/// <summary>最後に持ち上げられた時刻(運んでいた時間を出すため)</summary>
public float lastTakenS = -1f;
public bool fragile; // デザート・ドリンク
public bool taken; // 誰かが持っている
public bool byRobot; // ロボットが運んだ
public int requeued; // 受け渡し口に戻された回数
}
/// <summary>スタッフの仕事。優先は試算の staff_job と同じ(1=案内・配膳・呼び出し・会計、2=片付け)</summary>
public enum StaffJobKind { Guide, Serve, Call, Pay, Clear }
public class StaffJob
{
public StaffJobKind kind;
public int priority;
public int groupId;
public int tableIndex;
public int tableId;
public DishOrder dish; // Serve のときだけ
public bool taken;
public float postedS;
/// <summary>仕事が終わったときに呼ばれる(出した側が入れる)</summary>
public System.Action onDone;
}
/// <summary>
/// あきらめた・移した・卓を空けた、の1回ぶん(give_ups.csv に1行)。
/// 「移した」「卓を空けた」が多いときは、人が通れない店になっているしるし。
/// </summary>
public struct GiveUpRow
{
public float tS; // 店の中の時刻(秒)
public string who; // 客 / スタッフ / 組
public string job; // 何をしていたか
public int tableId; // 関わる卓(無ければ 0)
public float toX, toZ; // 向かっていた先
public float x, z; // そのときの位置
public string aisle; // そのときいた通路
public float waitedS; // 着けないまま待った秒数
public string action; // あきらめた / 移した / 卓を空けた
}
/// <summary>
/// 人がロボットに近づきすぎた1回ぶん(contacts.csv に1行)。
/// ロボットの外周から PeopleSpec.robotContactGapM 以内に入ったとき。
/// </summary>
public struct ContactRow
{
public float tS; // 店の中の時刻(秒)
public float x, z; // そのときの位置
public string aisle; // そのときいた通路
public string place; // そのときいた場所(レジ・待合・ドリンクバーなど。無ければ空)
public string who; // 誰が(種類:名前)
public bool careless; // よけ方を守らない人だったか
public int robotId; // 相手の台
public float gapM; // ロボットの外周からの空き(m。負なら重なった)
}
/// <summary>届けられずに受け渡し口へ戻した1回ぶん(dishes.csv に1行)</summary>
public struct RequeueRow
{
public float tS;
public int dishId;
public int tableId;
public int robotId;
public bool fragile;
public float readyS; // 料理ができた時刻
public int requeuedSoFar; // この皿が戻された回数(この回を含む)
public string why; // 動けなくなった / 打ち切り / 取りやめ / 着かなかった
}
/// <summary>卓1つの状態</summary>
public class TableSlot
{
public int index;
public int tableId;
public Vector3 deliveryPoint;
public Vector3 aisleEntrance;
public Bounds aisleZone;
public Vector3 seatPoint;
/// <summary>何人かけの卓か(2人席なら2、4人席なら4)</summary>
public int seatCount = 4;
public int groupId = -1; // -1 なら空き
public bool clearing; // 片付け中(まだ使えない)
public bool Free { get { return groupId < 0 && !clearing; } }
}
/// <summary>
/// 店の回し方の中心。卓の空き、受け渡し口の料理の待ち行列、スタッフの仕事板を持ち、
/// 空いたロボットに料理をまとめて渡す(試算の deliver / _robot_run と同じ決まり)。
///
/// この script は店の運営だけを扱う。人の歩き方はそれぞれの script、
/// ロボットの止まる決まりは ServingRobotController に任せる。
/// 数字は ServiceSpec の .asset に置く。
/// </summary>
public class ServiceOps : MonoBehaviour
{
[Header("設定")]
public ServiceSpec spec;
public RobotSpec robotSpec;
[Tooltip("卓と通路を読む先")]
public StoreBuilder store;
[Header("いまの様子(読むだけ)")]
[SerializeField] int _freeTables;
[SerializeField] int _dishesWaiting;
[SerializeField] int _jobsWaiting;
readonly List<TableSlot> _tables = new List<TableSlot>();
readonly List<DishOrder> _pass = new List<DishOrder>();
readonly List<StaffJob> _jobs = new List<StaffJob>();
readonly List<ServingRobotController> _robots = new List<ServingRobotController>();
readonly Dictionary<ServingRobotController, List<DishOrder>> _carrying =
new Dictionary<ServingRobotController, List<DishOrder>>();
readonly Dictionary<ServingRobotController, List<DishOrder>> _lastBatch =
new Dictionary<ServingRobotController, List<DishOrder>>();
int _nextDishId = 1;
// ---- 記録(TripLogger と ExperimentRunner が読む)----
public readonly List<float> dishWaitMin = new List<float>(); // 料理ができてから届くまで(分)
public readonly List<float> seatWaitMin = new List<float>(); // 入店から着席まで(分)
public readonly List<float> turnMin = new List<float>(); // 退店から卓が空くまで(分)
public int robotDishes { get; private set; }
public int staffDishes { get; private set; }
public int requeuedDishes { get; private set; }
// ---- あきらめた記録(B1)----
readonly List<GiveUpRow> _giveUps = new List<GiveUpRow>();
public IReadOnlyList<GiveUpRow> GiveUps { get { return _giveUps; } }
// ---- 人がロボットをよけた記録 ----
readonly List<ContactRow> _contacts = new List<ContactRow>();
/// <summary>人がロボットに近づきすぎた1回ずつ(contacts.csv)</summary>
public IReadOnlyList<ContactRow> Contacts { get { return _contacts; } }
/// <summary>人がロボットを待った回数(入口で待つ+通路の中で下がる)</summary>
public int robotYieldWaits { get; private set; }
/// <summary>人がロボットを待っていた秒数の合計</summary>
public float robotYieldWaitSeconds { get; private set; }
/// <summary>人が回り道した回数</summary>
public int robotYieldDetours { get; private set; }
// ---- レジの前の混み具合(調べるための記録)----
/// <summary>レジの前にいた人数のいちばん多いとき</summary>
public int registerCrowdMax { get; private set; }
float _registerCrowdSum;
int _registerCrowdSamples;
// ---- 会計のときスタッフが店員側(レジの内側)にいたか ----
/// <summary>会計をした回数</summary>
public int registerPayCount { get; private set; }
/// <summary>そのうち、店員側にいなかった回数(0 になるはず)</summary>
public int registerPayNotAtBackCount { get; private set; }
/// <summary>立つ点から 0.1 m より離れて会計した回数(0 になるはず)</summary>
public int registerPayOffSpotCount { get; private set; }
/// <summary>立つ点と実際に立った位置の距離のいちばん大きいもの(m)</summary>
public float registerPayMaxOffsetM { get; private set; }
/// <summary>立つ点からの距離の合計(平均を出すため, m)</summary>
float _registerPayOffsetSum;
/// <summary>
/// いま店員側(レジの内側)で会計しているスタッフがいるか。
/// 店員側は 0.8 × 1.2 m で1人ぶんなので、会計の仕事は同時に1つだけ取れる。
/// </summary>
public bool RegisterBackBusy { get; private set; }
/// <summary>店員側に、会計でないスタッフがいた秒数(0 になるはず)</summary>
public float registerBackIntruderSeconds { get; private set; }
/// <summary>店員側に、会計でないスタッフがいた回数(0 になるはず)</summary>
public int registerBackIntruderCount { get; private set; }
/// <summary>会計でないスタッフが店員側にいたことを残す(StaffAgent が毎コマ呼ぶ)</summary>
public void AddRegisterBackIntruder(float dt, bool firstFrame)
{
registerBackIntruderSeconds += dt;
if (firstFrame) registerBackIntruderCount++;
}
/// <summary>立つ点と実際に立った位置の距離の平均(m)</summary>
public float RegisterPayMeanOffsetM
{
get { return registerPayCount > 0 ? _registerPayOffsetSum / registerPayCount : 0f; }
}
/// <summary>
/// 会計1回ぶんを数える。
/// atBack が false なら店員側にいなかった。offsetM は立つ点からの距離。
/// どちらも 0 になるはず(ならなければ、スタッフが店員側に入れていない)。
/// </summary>
public void AddRegisterPay(bool atBack, float offsetM)
{
registerPayCount++;
if (!atBack) registerPayNotAtBackCount++;
_registerPayOffsetSum += offsetM;
if (offsetM > registerPayMaxOffsetM) registerPayMaxOffsetM = offsetM;
float limit = spec != null ? spec.registerPayOffSpotM : 0.1f;
if (offsetM > limit) registerPayOffSpotCount++;
}
/// <summary>レジの前にいた人数の平均</summary>
public float RegisterCrowdMean
{
get { return _registerCrowdSamples > 0 ? _registerCrowdSum / _registerCrowdSamples : 0f; }
}
// ---- スタッフの時間の使い方(仕事ごとの秒数)----
readonly Dictionary<string, float> _staffTime = new Dictionary<string, float>();
/// <summary>スタッフが何にどれだけ時間を使ったか(仕事の名前 → 秒)</summary>
public IDictionary<string, float> StaffTime { get { return _staffTime; } }
// ---- 料理が届くまでの内訳(運んだのがロボットか人かで分ける)----
/// <summary>届いた料理1皿ぶんの内訳(秒)</summary>
public struct DishParts
{
public bool byRobot;
public float passWaitS; // 受け渡し口で待った(できてから、はじめて持ち上げられるまで)
public float carryS; // 運んでいた(最後に持ち上げられてから、卓に届くまで)
public float requeueS; // 途中で戻されて失った(残り)
public int requeued; // 戻された回数
}
readonly List<DishParts> _dishParts = new List<DishParts>();
/// <summary>届いた料理の、届くまでの内訳(summary.json に出す)</summary>
public IReadOnlyList<DishParts> DishPartsList { get { return _dishParts; } }
// ---- 仕事が板で待った秒数(仕事ごと)----
readonly Dictionary<string, float> _jobWaitSum = new Dictionary<string, float>();
readonly Dictionary<string, int> _jobWaitCount = new Dictionary<string, int>();
/// <summary>仕事が板に出てから取られるまでの秒数の合計(仕事の名前 → 秒)</summary>
public IDictionary<string, float> JobWaitSum { get { return _jobWaitSum; } }
/// <summary>仕事が取られた回数(仕事の名前 → 回)</summary>
public IDictionary<string, int> JobWaitCount { get { return _jobWaitCount; } }
void RecordJobWait(StaffJob j)
{
if (j == null) return;
string k = StaffAgent.JobKindName(j.kind);
float v; int n;
_jobWaitSum.TryGetValue(k, out v);
_jobWaitCount.TryGetValue(k, out n);
_jobWaitSum[k] = v + Mathf.Max(0f, NowS - j.postedS);
_jobWaitCount[k] = n + 1;
}
/// <summary>スタッフの時間を足す(StaffAgent が毎コマ呼ぶ)</summary>
public void AddStaffTime(string what, float dt)
{
if (string.IsNullOrEmpty(what)) return;
float v;
_staffTime.TryGetValue(what, out v);
_staffTime[what] = v + dt;
}
// ---- レジの前に並ぶ点(会計に立つ人が1人ずつ)----
readonly List<Vector3> _regQueues = new List<Vector3>();
readonly List<bool> _regQueueUsed = new List<bool>();
/// <summary>レジの並ぶ点を1つ取る。無ければ -1(会計に立つ人も席で待つ)</summary>
public int TakeRegisterQueueSpot(out Vector3 spot)
{
for (int i = 0; i < _regQueueUsed.Count; i++)
{
if (_regQueueUsed[i]) continue;
_regQueueUsed[i] = true;
spot = _regQueues[i];
return i;
}
spot = RegisterPointOrPass();
return -1;
}
/// <summary>取ったレジの並ぶ点を空ける</summary>
public void ReleaseRegisterQueueSpot(int index)
{
if (index < 0 || index >= _regQueueUsed.Count) return;
_regQueueUsed[index] = false;
}
Vector3 RegisterPointOrPass()
{
return store != null ? store.RegisterPoint : Vector3.zero;
}
public int gaveUpCount { get; private set; } // あきらめた
public int warpedCount { get; private set; } // 移した
public int tableFreedCount { get; private set; } // 卓を空けた
// ---- 戻した皿の記録(B8)----
readonly List<RequeueRow> _requeues = new List<RequeueRow>();
public IReadOnlyList<RequeueRow> Requeues { get { return _requeues; } }
// ---- 椅子を引いた回数(B6)----
readonly Dictionary<string, int> _chairPullsByReason = new Dictionary<string, int>();
readonly Dictionary<int, int> _chairPullsByTable = new Dictionary<int, int>();
public int chairPulls { get; private set; }
public IDictionary<string, int> ChairPullsByReason { get { return _chairPullsByReason; } }
public IDictionary<int, int> ChairPullsByTable { get { return _chairPullsByTable; } }
// ---- ドリンクバーの場所取り(B4)----
readonly List<Vector3> _dbStands = new List<Vector3>();
readonly List<Vector3> _dbQueues = new List<Vector3>();
readonly List<bool> _dbStandUsed = new List<bool>();
readonly List<bool> _dbQueueUsed = new List<bool>();
/// <summary>並ぶ点に回された回数</summary>
public int drinkBarQueued { get; private set; }
/// <summary>立つ点も並ぶ点も空いていなくて、あきらめた回数</summary>
public int drinkBarGaveUp { get; private set; }
/// <summary>並んで待った秒数(分で記録)</summary>
public readonly List<float> drinkBarWaitMin = new List<float>();
public IList<ServingRobotController> Robots { get { return _robots; } }
public IList<TableSlot> Tables { get { return _tables; } }
public int FreeTableCount { get { return _freeTables; } }
public int DishesWaiting { get { return _dishesWaiting; } }
/// <summary>店の中の時刻(秒)。ExperimentRunner が進める</summary>
public float NowS { get; private set; }
// ---------------- 用意 ----------------
public void Setup()
{
_tables.Clear();
_pass.Clear();
_jobs.Clear();
_carrying.Clear();
dishWaitMin.Clear();
seatWaitMin.Clear();
turnMin.Clear();
robotDishes = 0;
staffDishes = 0;
requeuedDishes = 0;
_giveUps.Clear();
_requeues.Clear();
_contacts.Clear();
_staffTime.Clear();
_dishParts.Clear();
_jobWaitSum.Clear();
_jobWaitCount.Clear();
registerCrowdMax = 0;
registerPayCount = 0;
registerPayNotAtBackCount = 0;
registerPayOffSpotCount = 0;
registerPayMaxOffsetM = 0f;
_registerPayOffsetSum = 0f;
RegisterBackBusy = false;
registerBackIntruderSeconds = 0f;
registerBackIntruderCount = 0;
_registerCrowdSum = 0f;
_registerCrowdSamples = 0;
_regQueues.Clear();
_regQueueUsed.Clear();
robotYieldWaits = 0;
robotYieldWaitSeconds = 0f;
robotYieldDetours = 0;
_chairPullsByReason.Clear();
_chairPullsByTable.Clear();
chairPulls = 0;
gaveUpCount = 0;
warpedCount = 0;
tableFreedCount = 0;
_nextDishId = 1;
NowS = 0f;
drinkBarQueued = 0;
drinkBarGaveUp = 0;
drinkBarWaitMin.Clear();
_dbStands.Clear();
_dbQueues.Clear();
_dbStandUsed.Clear();
_dbQueueUsed.Clear();
if (store == null) return;
IList<StoreBuilder.TableInfo> t = store.Tables;
for (int i = 0; i < t.Count; i++)
{
TableSlot s = new TableSlot();
s.index = i;
s.tableId = t[i].tableId;
s.deliveryPoint = t[i].deliveryPoint;
s.aisleEntrance = t[i].aisleEntrance;
s.aisleZone = t[i].aisleZone;
s.seatPoint = t[i].center;
s.seatCount = t[i].seatCount;
_tables.Add(s);
}
// ドリンクバーの立つ点・並ぶ点を読む(B4)
SetupDrinkBar(store.PointsOf(PlaceKind.DrinkBarStand),
store.PointsOf(PlaceKind.DrinkBarQueue));
// レジの前に並ぶ点を読む
IList<Vector3> rq = store.RegisterQueuePoints;
for (int i = 0; i < rq.Count; i++) { _regQueues.Add(rq[i]); _regQueueUsed.Add(false); }
Recount();
}
// ---------------- ドリンクバーの場所取り(B4)----------------
void SetupDrinkBar(IList<Vector3> stands, IList<Vector3> queues)
{
for (int i = 0; i < stands.Count; i++) { _dbStands.Add(stands[i]); _dbStandUsed.Add(false); }
for (int i = 0; i < queues.Count; i++) { _dbQueues.Add(queues[i]); _dbQueueUsed.Add(false); }
}
/// <summary>
/// ドリンクバーの場所を1つ取る(B4)。
/// ・空いている立つ点があれば、そこへ
/// ・埋まっていれば、端の並ぶ点へ
/// ・それも埋まっていれば取れない(客はあきらめて席に戻る)
/// spot は向かう点、queued は並ぶ点だったか。
/// 返した番号を ReleaseDrinkBarSpot に渡して空ける。
/// </summary>
public int TakeDrinkBarSpot(out Vector3 spot, out bool queued)
{
for (int i = 0; i < _dbStandUsed.Count; i++)
{
if (_dbStandUsed[i]) continue;
_dbStandUsed[i] = true;
spot = _dbStands[i];
queued = false;
return i;
}
for (int i = 0; i < _dbQueueUsed.Count; i++)
{
if (_dbQueueUsed[i]) continue;
_dbQueueUsed[i] = true;
spot = _dbQueues[i];
queued = true;
drinkBarQueued++;
return 1000 + i; // 1000 以上は並ぶ点
}
spot = _dbStands.Count > 0 ? _dbStands[0] : Vector3.zero;
queued = false;
drinkBarGaveUp++;
return -1;
}
/// <summary>取った場所を空ける。waitedS は並んで待った秒数</summary>
public void ReleaseDrinkBarSpot(int token, float waitedS)
{
if (token < 0) return;
if (token >= 1000)
{
int i = token - 1000;
if (i < _dbQueueUsed.Count) _dbQueueUsed[i] = false;
drinkBarWaitMin.Add(Mathf.Max(0f, waitedS / 60f));
return;
}
if (token < _dbStandUsed.Count) _dbStandUsed[token] = false;
}
/// <summary>ドリンクバーに立つ点があるか(無ければ今までどおり1点へ行く)</summary>
public bool HasDrinkBarSpots { get { return _dbStands.Count > 0; } }
public void RegisterRobot(ServingRobotController r)
{
if (r == null || _robots.Contains(r)) return;
_robots.Add(r);
_carrying[r] = new List<DishOrder>();
r.TripCompleted += OnRobotRunDone;
}
public void ClearRobots()
{
for (int i = 0; i < _robots.Count; i++)
if (_robots[i] != null) _robots[i].TripCompleted -= OnRobotRunDone;
_robots.Clear();
_carrying.Clear();
}
/// <summary>店の中の時刻を進める(ExperimentRunner が毎コマ呼ぶ)</summary>
public void Advance(float dt)
{
NowS += dt;
AssignRobots();
ParkIdleRobots();
SampleRegisterCrowd();
Recount();
}
/// <summary>
/// レジの前にいた人数を数える(調べるための記録)。
/// 待合に立つ客も、通路を歩く人も、スタッフも数える。
/// </summary>
void SampleRegisterCrowd()
{
if (store == null || spec == null) return;
if (!store.HasPoint(PlaceKind.Register)) return;
Vector3 reg = store.RegisterPoint;
float r2 = spec.registerCrowdRadiusM * spec.registerCrowdRadiusM;
int n = 0;
IReadOnlyList<PersonBody> all = PersonBody.All;
for (int i = 0; i < all.Count; i++)
{
PersonBody b = all[i];
if (b == null || b.kind == PersonKind.Seated) continue;
Vector3 d = b.transform.position - reg;
d.y = 0f;
if (d.sqrMagnitude <= r2) n++;
}
if (n > registerCrowdMax) registerCrowdMax = n;
_registerCrowdSum += n;
_registerCrowdSamples++;
}
/// <summary>
/// 仕事のないロボットを待機場所へ下げる(B2)。
/// 受け渡し口の前の点にはスタッフも料理を取りに来るので、空の台を置かない。
/// 料理を載せるときは AssignRobots が受け渡し口から走り出させる。
/// </summary>
void ParkIdleRobots()
{
if (store == null) return;
IList<Vector3> spots = store.RobotStandbyPoints;
if (spots.Count == 0) return;
for (int i = 0; i < _robots.Count; i++)
{
ServingRobotController r = _robots[i];
if (r == null) continue;
if (r.CurrentPhase != ServingRobotController.Phase.Idle &&
r.CurrentPhase != ServingRobotController.Phase.Done) continue;
// 載せる料理が待っているなら、下がらずに受け渡し口で待つ
if (_dishesWaiting > 0) continue;
Vector3 spot = spots[i % spots.Count];
r.GoStandby(spot);
}
}
void Recount()
{
int free = 0;
for (int i = 0; i < _tables.Count; i++) if (_tables[i].Free) free++;
_freeTables = free;
int d = 0;
for (int i = 0; i < _pass.Count; i++) if (!_pass[i].taken) d++;
_dishesWaiting = d;
int j = 0;
for (int i = 0; i < _jobs.Count; i++) if (!_jobs[i].taken) j++;
_jobsWaiting = j;
}
// ---------------- 卓 ----------------
/// <summary>
/// その人数が座れる空き卓を1つ取る。無ければ null。
/// 「入るうちでいちばん小さい卓」を選ぶので、
/// ・1〜2人の組は空いている2人席を先に使い、無ければ4人席
/// ・3〜4人の組は4人席だけ
/// になる(2人席に3人以上は座らせない)。
/// </summary>
public TableSlot TakeFreeTable(int groupId, int size)
{
TableSlot slot = FindFreeTable(size);
if (slot == null) return null;
slot.groupId = groupId;
Recount();
return slot;
}
/// <summary>その人数が入るうちで、いちばん席数の少ない空き卓。無ければ null</summary>
TableSlot FindFreeTable(int size)
{
int want = Mathf.Max(1, size);
TableSlot best = null;
int bestSeats = int.MaxValue;
for (int i = 0; i < _tables.Count; i++)
{
if (!_tables[i].Free) continue;
int seats = Mathf.Max(1, _tables[i].seatCount);
if (seats < want) continue;
if (seats >= bestSeats) continue;
best = _tables[i];
bestSeats = seats;
if (bestSeats == want) break; // ちょうどの大きさが見つかった
}
return best;
}
/// <summary>その人数が座れる空き卓があるか</summary>
public bool HasFreeTableFor(int size)
{
return FindFreeTable(size) != null;
}
/// <summary>慣らしのために、卓を使っている状態で始める</summary>
public TableSlot TakeTableAt(int index, int groupId)
{
if (index < 0 || index >= _tables.Count) return null;
if (!_tables[index].Free) return null;
_tables[index].groupId = groupId;
Recount();
return _tables[index];
}
/// <summary>片付けが終わって卓が空く</summary>
public void ReleaseTable(TableSlot slot, float leftS)
{
if (slot == null) return;
slot.groupId = -1;
slot.clearing = false;
if (leftS >= 0f) turnMin.Add((NowS - leftS) / 60f);
Recount();
}
public void RecordSeatWait(float arriveS)
{
seatWaitMin.Add(Mathf.Max(0f, (NowS - arriveS) / 60f));
}
// ---------------- あきらめた記録(B1)----------------
/// <summary>あきらめた・移した・卓を空けたことを1行残す</summary>
public void AddGiveUp(string who, string job, int tableId, Vector3 to, Vector3 at,
float waitedS, string action)
{
GiveUpRow r = new GiveUpRow();
r.tS = NowS;
r.who = who;
r.job = job;
r.tableId = tableId;
r.toX = to.x;
r.toZ = to.z;
r.x = at.x;
r.z = at.z;
r.aisle = AisleNameAt(at);
r.waitedS = waitedS;
r.action = action;
_giveUps.Add(r);
if (action == "移した") warpedCount++;
else if (action == "卓を空けた") tableFreedCount++;
else gaveUpCount++;
}
// ---------------- 人がロボットをよけた記録 ----------------
/// <summary>人がロボットの外周の近くまで来た1回を残す(接触に近い)</summary>
public void AddRobotContact(Vector3 at, string who, bool careless, int robotId, float gapM)
{
ContactRow r = new ContactRow();
r.tS = NowS;
r.x = at.x;
r.z = at.z;
r.aisle = AisleNameAt(at);
r.place = store != null ? store.PlaceNameAt(at) : "";
r.who = who;
r.careless = careless;
r.robotId = robotId;
r.gapM = gapM;
_contacts.Add(r);
}
/// <summary>人がロボットを待ち始めた(入口で待つ・通路の中で下がる)</summary>
public void AddRobotYieldWait() { robotYieldWaits++; }
/// <summary>人がロボットを待っていた秒数を足す</summary>
public void AddRobotYieldWaitSeconds(float dt) { robotYieldWaitSeconds += dt; }
/// <summary>人が回り道した</summary>
public void AddRobotYieldDetour() { robotYieldDetours++; }
// ---------------- 椅子を引いた回数(B6)----------------
/// <summary>椅子を1脚引いた。きっかけごと・卓ごとに数える</summary>
public void AddChairPull(string reason, int tableId)
{
chairPulls++;
string key = string.IsNullOrEmpty(reason) ? "その他" : reason;
int n;
_chairPullsByReason[key] = _chairPullsByReason.TryGetValue(key, out n) ? n + 1 : 1;
_chairPullsByTable[tableId] = _chairPullsByTable.TryGetValue(tableId, out n) ? n + 1 : 1;
}
/// <summary>その点がどの通路の上かを名前で返す。細い通路を先に見る</summary>
public string AisleNameAt(Vector3 p)
{
if (store == null) return "";
string wide = "";
IList<StoreBuilder.AisleInfo> aisles = store.Aisles;
for (int i = 0; i < aisles.Count; i++)
{
Bounds b = aisles[i].zone;
if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue;
if (aisles[i].narrow) return aisles[i].name;
if (wide.Length == 0) wide = aisles[i].name;
}
return wide;
}
// ---------------- 料理 ----------------
/// <summary>料理が1皿できて、受け渡し口に出る</summary>
public DishOrder PutDish(int groupId, TableSlot slot, bool fragile)
{
DishOrder d = new DishOrder();
d.id = _nextDishId++;
d.groupId = groupId;
d.tableIndex = slot.index;
d.tableId = slot.tableId;
d.readyS = NowS;
d.fragile = fragile;
_pass.Add(d);
// スタッフの仕事にも出しておく。ロボットとスタッフの先に空いた方が運ぶ
// (ロボットが運べない料理は、ロボットの取り分けで外す)
PostJob(StaffJobKind.Serve, groupId, slot, d, 1);
Recount();
return d;
}
/// <summary>
/// 慣らしのために、もう届いている皿として1枚作る(B3)。
/// 受け渡し口にも仕事板にも出さず、待ち時間の記録にも入れない
/// (慣らしの皿を 0 分として数えると dish_wait が小さく出てしまう)。
/// </summary>
public DishOrder SeedArrivedDish(int groupId, TableSlot slot)
{
DishOrder d = new DishOrder();
d.id = _nextDishId++;
d.groupId = groupId;
d.tableIndex = slot != null ? slot.index : -1;
d.tableId = slot != null ? slot.tableId : 0;
d.readyS = NowS;
d.arrivedS = NowS;
d.fragile = false;
return d;
}
/// <summary>届いた(ロボットでもスタッフでも)</summary>
/// <summary>料理を持ち上げた。受け渡し口で待った時間・運んでいた時間を出すために時刻を残す</summary>
void MarkTaken(DishOrder d)
{
if (d == null) return;
d.taken = true;
if (d.firstTakenS < 0f) d.firstTakenS = NowS;
d.lastTakenS = NowS;
}
public void DishArrived(DishOrder d, bool byRobot)
{
if (d == null || d.arrivedS >= 0f) return;
d.arrivedS = NowS;
d.byRobot = byRobot;
dishWaitMin.Add(Mathf.Max(0f, (d.arrivedS - d.readyS) / 60f));
if (byRobot) robotDishes++; else staffDishes++;
AddDishParts(d);
_pass.Remove(d);
RemoveServeJob(d);
Recount();
}
/// <summary>
/// 届けられなかった料理を、受け渡し口の待ち行列の先頭に戻す。
/// 次に空いたロボットかスタッフが運ぶ。
/// </summary>
/// <summary>
/// 届いた1皿の、届くまでの内訳を足す。
/// 受け渡し口で待った + 運んでいた + 途中で戻されて失った = できてから届くまで
/// </summary>
void AddDishParts(DishOrder d)
{
if (d == null || d.arrivedS < 0f || d.readyS < 0f) return;
float total = Mathf.Max(0f, d.arrivedS - d.readyS);
float passWait = d.firstTakenS >= 0f ? Mathf.Max(0f, d.firstTakenS - d.readyS) : total;
float carry = d.lastTakenS >= 0f ? Mathf.Max(0f, d.arrivedS - d.lastTakenS) : 0f;
DishParts p = new DishParts();
p.byRobot = d.byRobot;
p.passWaitS = passWait;
p.carryS = carry;
p.requeueS = Mathf.Max(0f, total - passWait - carry);
p.requeued = d.requeued;
_dishParts.Add(p);
}
public void RequeueDish(DishOrder d)
{
RequeueDish(d, 0, "");
}
/// <summary>戻した理由も残す(B8)。robotId 0 はロボット以外</summary>
public void RequeueDish(DishOrder d, int robotId, string why)
{
if (d == null || d.arrivedS >= 0f) return;
d.taken = false;
d.requeued++;
requeuedDishes++;
RequeueRow r = new RequeueRow();
r.tS = NowS;
r.dishId = d.id;
r.tableId = d.tableId;
r.robotId = robotId;
r.fragile = d.fragile;
r.readyS = d.readyS;
r.requeuedSoFar = d.requeued;
r.why = string.IsNullOrEmpty(why) ? "着かなかった" : why;
_requeues.Add(r);
_pass.Remove(d);
_pass.Insert(0, d);
TableSlot slot = SlotOf(d.tableIndex);
RemoveServeJob(d);
if (slot != null) PostJobFront(StaffJobKind.Serve, d.groupId, slot, d, 1);
Recount();
}
TableSlot SlotOf(int index)
{
return index >= 0 && index < _tables.Count ? _tables[index] : null;
}
// ---------------- スタッフの仕事板 ----------------
public StaffJob PostJob(StaffJobKind kind, int groupId, TableSlot slot, DishOrder dish, int priority)
{
StaffJob j = NewJob(kind, groupId, slot, dish, priority);
_jobs.Add(j);
Recount();
return j;
}
StaffJob PostJobFront(StaffJobKind kind, int groupId, TableSlot slot, DishOrder dish, int priority)
{
StaffJob j = NewJob(kind, groupId, slot, dish, priority);
_jobs.Insert(0, j);
Recount();
return j;
}
StaffJob NewJob(StaffJobKind kind, int groupId, TableSlot slot, DishOrder dish, int priority)
{
StaffJob j = new StaffJob();
j.kind = kind;
j.priority = priority;
j.groupId = groupId;
j.tableIndex = slot != null ? slot.index : -1;
j.tableId = slot != null ? slot.tableId : 0;
j.dish = dish;
j.postedS = NowS;
return j;
}
/// <summary>
/// 次にやる仕事を1つ取る。優先度が小さいものが先、同じなら出た順。
/// 配膳の仕事は、料理がまだ誰も持っていないものだけ。
/// </summary>
public StaffJob TakeNextJob()
{
StaffJob best = null;
for (int i = 0; i < _jobs.Count; i++)
{
StaffJob j = _jobs[i];
if (j.taken) continue;
if (j.kind == StaffJobKind.Serve && (j.dish == null || j.dish.taken || j.dish.arrivedS >= 0f)) continue;
// 店員側(レジの内側)は1人ぶんなので、会計はひとつずつ
if (j.kind == StaffJobKind.Pay && RegisterBackBusy) continue;
if (best == null || j.priority < best.priority) best = j;
if (best != null && best.priority <= 1) break; // 優先1が見つかれば、出た順で先頭
}
if (best == null) return null;
best.taken = true;
if (best.kind == StaffJobKind.Pay) RegisterBackBusy = true;
RecordJobWait(best);
if (best.kind == StaffJobKind.Serve && best.dish != null) MarkTaken(best.dish);
Recount();
return best;
}
/// <summary>仕事が終わった</summary>
public void FinishJob(StaffJob job)
{
if (job == null) return;
if (job.kind == StaffJobKind.Pay) RegisterBackBusy = false; // 店員側を空ける
_jobs.Remove(job);
Recount();
if (job.onDone != null) job.onDone();
}
/// <summary>
/// 仕事を板から外すだけ(終わったときの呼び出しはしない)。
/// 卓を強引に空けたときなど、出した側がもう待っていないときに使う。
/// </summary>
public void FinishJobSilently(StaffJob job)
{
if (job == null) return;
if (job.kind == StaffJobKind.Pay) RegisterBackBusy = false; // 店員側を空ける
job.onDone = null;
_jobs.Remove(job);
if (job.kind == StaffJobKind.Serve && job.dish != null) job.dish.taken = false;
Recount();
}
/// <summary>仕事を手放す(届けられなかったときなど)</summary>
public void AbandonJob(StaffJob job)
{
if (job == null) return;
job.taken = false;
if (job.kind == StaffJobKind.Pay) RegisterBackBusy = false; // 店員側を空ける
if (job.kind == StaffJobKind.Serve && job.dish != null) job.dish.taken = false;
Recount();
}
void RemoveServeJob(DishOrder d)
{
for (int i = _jobs.Count - 1; i >= 0; i--)
if (_jobs[i].kind == StaffJobKind.Serve && _jobs[i].dish == d) _jobs.RemoveAt(i);
}
// ---------------- ロボットに渡す ----------------
/// <summary>
/// 空いているロボットに、受け渡し口の料理を段の数までまとめて渡す。
/// 近い卓から回る(試算の _robot_run と同じ)。
/// </summary>
void AssignRobots()
{
if (spec == null || store == null) return;
for (int i = 0; i < _robots.Count; i++)
{
ServingRobotController r = _robots[i];
if (r == null) continue;
if (r.CurrentPhase != ServingRobotController.Phase.Idle &&
r.CurrentPhase != ServingRobotController.Phase.Done) continue;
List<DishOrder> batch = PickBatch();
if (batch.Count == 0) continue;
List<ServingRobotController.RunStop> stops = new List<ServingRobotController.RunStop>();
for (int k = 0; k < batch.Count; k++)
{
TableSlot s = SlotOf(batch[k].tableIndex);
if (s == null) continue;
ServingRobotController.RunStop stop = new ServingRobotController.RunStop();
stop.tableId = s.tableId;
stop.point = s.deliveryPoint;
stop.aisleEntrance = s.aisleEntrance;
stop.aisleZone = s.aisleZone;
stops.Add(stop);
}
if (stops.Count == 0) continue;
_carrying[r] = batch;
r.StartRun(stops, store.PassPoint, NowS);
}
}
/// <summary>段の数までまとめる。近い卓から順に並べる</summary>
List<DishOrder> PickBatch()
{
List<DishOrder> batch = new List<DishOrder>();
Vector3 pass = store.PassPoint;
int trays = Mathf.Max(1, spec.trays);
for (int i = 0; i < _pass.Count && batch.Count < trays; i++)
{
DishOrder d = _pass[i];
if (d.taken || d.arrivedS >= 0f) continue;
if (!spec.fragileByRobot && d.fragile) continue; // 人だけが運ぶ料理
batch.Add(d);
}
batch.Sort(delegate (DishOrder a, DishOrder b)
{
TableSlot sa = SlotOf(a.tableIndex), sb = SlotOf(b.tableIndex);
float da = sa != null ? Flat(sa.deliveryPoint - pass) : 0f;
float db = sb != null ? Flat(sb.deliveryPoint - pass) : 0f;
return da.CompareTo(db);
});
for (int i = 0; i < batch.Count; i++)
{
MarkTaken(batch[i]);
RemoveServeJob(batch[i]);
}
return batch;
}
static float Flat(Vector3 d) { d.y = 0f; return d.magnitude; }
/// <summary>
/// ロボットの1走りが終わった。届けた料理を記録し、
/// 届けられなかった料理は受け渡し口の先頭に戻す。
/// </summary>
void OnRobotRunDone(ServingRobotController r)
{
List<DishOrder> batch;
if (!_carrying.TryGetValue(r, out batch) || batch == null) return;
_lastBatch[r] = batch; // trips.csv を書くために残す
_carrying[r] = new List<DishOrder>();
// なぜ届けられなかったか。ロボットが持っている結果から決める
string why = r.Stuck ? "動けなくなった"
: r.TimedOut ? "打ち切り"
: r.Aborted ? "取りやめ"
: "着かなかった";
int done = Mathf.Clamp(r.StopsCompleted, 0, batch.Count);
for (int i = 0; i < batch.Count; i++)
{
if (i < done) DishArrived(batch[i], true);
else RequeueDish(batch[i], r.robotId, why);
}
}
/// <summary>そのロボットがいま載せている料理</summary>
public IList<DishOrder> CarriedBy(ServingRobotController r)
{
List<DishOrder> batch;
if (r != null && _carrying.TryGetValue(r, out batch)) return batch;
return new List<DishOrder>();
}
/// <summary>直前の1走りで載せていた料理(trips.csv を書くのに使う)</summary>
public IList<DishOrder> LastBatchOf(ServingRobotController r)
{
List<DishOrder> batch;
if (r != null && _lastBatch.TryGetValue(r, out batch) && batch != null) return batch;
return new List<DishOrder>();
}
}
}
Service/ServiceSpec.asset
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 678ac8a5ea67d4f8c89127fd97fb60a1, type: 3}
m_Name: ServiceSpec
m_EditorClassIdentifier:
arrivalsPerHourByCrowd: 19000000230000002d000000
peakMinutes: 120
groupSizeWeights:
- 0.2
- 0.35
- 0.2
- 0.25
guideSec: 45
paySec: 90
callSec: 30
registerCrowdRadiusM: 2
payAtRegisterByOne: 1
registerPayOffSpotM: 0.1
callRate: 0.6
firstDish1CookMin: {x: 5, y: 8, z: 12}
firstDish2CookMin: {x: 8, y: 12, z: 18}
fragilePerGroup: 0.5
fragileCookMin: {x: 2, y: 4, z: 7}
fragileByRobot: 1
coldMin: 3
trays: 3
robotHandoffSec: 25
staffPlaceSec: 10
eatMin: {x: 20, y: 30, z: 45}
drinkTripsPerPerson: 1.5
drinkBarSec: 20
toiletTripsPerGroup: 0.3
toiletSec: 180
clearSec: 40
wipeSec: 40
busTrips: 2
waitingVisibleMax: 8
tableStuckFreeSec: 600
Service/ServiceSpec.cs
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 店の回し方の数字。試算(haizen_sim.py の COMMON と STORES["family"])と同じ値にそろえる。
/// 店の運営に関する数字はすべてこの .asset に置く。
///
/// 三角分布は(いちばん短い, いちばん多い, いちばん長い)の分(min)で持つ。
/// </summary>
[CreateAssetMenu(menuName = "ServiceSim/Service Spec", fileName = "ServiceSpec")]
public class ServiceSpec : ScriptableObject
{
[Header("来客(STORES[\"family\"][\"arrivals\"])")]
[Tooltip("混み具合ごとの来客のペース(組/時)。[空いている, 普通, 混雑]")]
public int[] arrivalsPerHourByCrowd = { 25, 35, 45 };
[Tooltip("ピークの長さ(分)。1回の実行の長さ(STORES[\"family\"][\"peak_min\"])")]
public float peakMinutes = 120f;
[Header("組(Unity だけの値。試算に人数はない)")]
[Tooltip("組の人数を選ぶ重み。[1人, 2人, 3人, 4人]")]
public float[] groupSizeWeights = { 0.20f, 0.35f, 0.20f, 0.25f };
[Header("案内・会計・呼び出し(COMMON)")]
[Tooltip("案内にかかる時間(秒)。歩く時間は別(guide_s)")]
public float guideSec = 45f;
[Tooltip("会計の時間(秒)(pay_s)")]
public float paySec = 90f;
[Tooltip("呼び出し対応の時間(秒)。歩く時間は別(call_s)")]
public float callSec = 30f;
[Tooltip("レジの前の人数を数える半径(m)。記録だけに使う")]
public float registerCrowdRadiusM = 2.0f;
[Tooltip("会計はレジに立つ組の1人だけで済ませる(ほかの人は席で待つ)。" +
"外すと前のとおり、誰もレジに行かずスタッフだけで会計する(前後を比べるときに使う)")]
public bool payAtRegisterByOne = true;
[Tooltip("会計のとき、スタッフが立つ点からこれ以上離れていたら数える(m)。" +
"店員側に入れていないしるしなので、0 回になるはず")]
public float registerPayOffSpotM = 0.1f;
[Tooltip("1組あたりの呼び出しの起きやすさ(call_rate)。試算は2回ぶん 0.6/2 で判定")]
public float callRate = 0.6f;
[Header("料理(first_dishes・extra・fragile)")]
[Tooltip("最初の注文:1皿めができるまで(分, 最短/最頻/最長)")]
public Vector3 firstDish1CookMin = new Vector3(5f, 8f, 12f);
[Tooltip("最初の注文:2皿めができるまで(分, 最短/最頻/最長)")]
public Vector3 firstDish2CookMin = new Vector3(8f, 12f, 18f);
[Tooltip("食事中のデザート・ドリンクの数の平均(fragile)")]
public float fragilePerGroup = 0.5f;
[Tooltip("デザート・ドリンクができるまで(分, 最短/最頻/最長)(fragile_cook)")]
public Vector3 fragileCookMin = new Vector3(2f, 4f, 7f);
[Tooltip("デザート・ドリンクをロボットも運ぶか。false なら人だけ(fragile_by)")]
public bool fragileByRobot = true;
[Tooltip("これを超えて届いた皿を「待たせた皿」とする(分)(cold_min)")]
public float coldMin = 3f;
[Header("運ぶ(COMMON)")]
[Tooltip("ロボットが1走りで載せる料理の数(trays)。1 か 3")]
public int trays = 3;
[Tooltip("客がロボットから皿を取る時間(秒)(robot_handoff_s)")]
public float robotHandoffSec = 25f;
[Tooltip("スタッフが皿を置く時間(秒)(staff_place_s)")]
public float staffPlaceSec = 10f;
[Header("食事(eat_min)")]
[Tooltip("食事の長さ(分, 最短/最頻/最長)")]
public Vector3 eatMin = new Vector3(20f, 30f, 45f);
[Tooltip("1人あたりドリンクバーへ行く平均の回数(Unity だけの値)")]
public float drinkTripsPerPerson = 1.5f;
[Tooltip("ドリンクバーで過ごす時間(秒)(Unity だけの値)")]
public float drinkBarSec = 20f;
[Tooltip("1組あたりトイレへ行く平均の回数(Unity だけの値)")]
public float toiletTripsPerGroup = 0.3f;
[Tooltip("トイレの中で過ごす時間(秒)(Unity だけの値)")]
public float toiletSec = 180f;
[Header("片付け(COMMON・bus_trips)")]
[Tooltip("卓の皿をまとめる時間(秒)(clear_s)")]
public float clearSec = 40f;
[Tooltip("卓を拭いてセットする時間(秒)(wipe_s)")]
public float wipeSec = 40f;
[Tooltip("下げ膳の往復回数(皿の量)(bus_trips)")]
public int busTrips = 2;
[Header("待合")]
[Tooltip("待合に立たせる組の上限。あふれた組は店の外で待つ扱い(人として置かない)")]
public int waitingVisibleMax = 8;
[Header("あきらめて、やり直す(B1)")]
[Tooltip("会計・片付けで止まって卓が空かない状態がこの秒数続いたら、卓を空ける")]
public float tableStuckFreeSec = 600f;
// ---- 読み出し ----
public int ArrivalsPerHour(CrowdLevel crowd)
{
if (arrivalsPerHourByCrowd == null || arrivalsPerHourByCrowd.Length == 0) return 35;
int i = Mathf.Clamp((int)crowd, 0, arrivalsPerHourByCrowd.Length - 1);
return arrivalsPerHourByCrowd[i];
}
/// <summary>三角分布(分)から秒を引く。試算の tri() と同じ</summary>
public static float TriangularSec(System.Random rng, Vector3 lowModeHigh)
{
float lo = lowModeHigh.x, mode = lowModeHigh.y, hi = lowModeHigh.z;
if (hi <= lo) return lo * 60f;
mode = Mathf.Clamp(mode, lo, hi);
double u = rng.NextDouble();
double c = (mode - lo) / (double)(hi - lo);
double v = u < c
? lo + System.Math.Sqrt(u * (hi - lo) * (mode - lo))
: hi - System.Math.Sqrt((1.0 - u) * (hi - lo) * (hi - mode));
return (float)v * 60f;
}
/// <summary>指数分布(秒)。試算の expovariate と同じ</summary>
public static float ExponentialSec(System.Random rng, float ratePerSec)
{
if (ratePerSec <= 0f) return float.MaxValue;
double u = rng.NextDouble();
if (u <= 1e-12) u = 1e-12;
return (float)(-System.Math.Log(u) / ratePerSec);
}
/// <summary>組の人数を重みで選ぶ</summary>
public int PickGroupSize(System.Random rng)
{
float total = 0f;
int n = groupSizeWeights != null ? groupSizeWeights.Length : 0;
for (int i = 0; i < n; i++) total += Mathf.Max(0f, groupSizeWeights[i]);
if (total <= 0f) return 2;
float r = (float)rng.NextDouble() * total;
for (int i = 0; i < n; i++)
{
r -= Mathf.Max(0f, groupSizeWeights[i]);
if (r <= 0f) return i + 1;
}
return n;
}
/// <summary>平均 mean 回を、整数回に落とす(試算の fragile と同じやり方)</summary>
public static int PickCount(System.Random rng, float mean)
{
int n = (int)mean;
if (rng.NextDouble() < mean - n) n++;
return n;
}
}
}
Experiment/ExperimentPlan.asset
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9d7ca72301ae84dbd8937d681f3df43d, type: 3}
m_Name: ExperimentPlan
m_EditorClassIdentifier:
runName: family_v0
aisleWidthIndices: 000000000100000002000000
crowdLevels: 000000000100000002000000
robotCounts: 0100000002000000
tripsPerCondition: 100
seeds: 0000000001000000020000000300000004000000
timeScale: 1
kioxiaVolumePath: /Volumes/KIOXIA
kioxiaOutputRoot: /Volumes/KIOXIA/r2r2r-data/service_sim/unity
trialAisleWidthIndex: 1
trialCrowd: 1
trialRobots: 2
trialSeed: 0
trialTrips: 20
trialTimeScaleFast: 10
trialCaptureDeltaTime: 0.02
trialMaxWallSeconds: 2400
trialOutputRoot: /Users/shimazueiichi/ServiceSim_tmp
measureMinutes: 120
warmupMinutes: 15
measureCaptureDeltaTime: 0.05
referenceCaptureDeltaTime: 0.02
disableRenderingWhileRunning: 0
showLabelsWhileRunning: 0
stage3OutputRoot: /Users/shimazueiichi/ServiceSim_tmp/stage3
stage3Resume: 1
screenshotDir: "/Users/shimazueiichi/Library/Mobile Documents/com~apple~CloudDocs/\u30C7\u30B9\u30AF\u30C8\u30C3\u30D7/ADAPT
DESIGN/\u3058\u3076\u3093\u306E/PixelstoMotors/\u30C7\u30B8\u30BF\u30EB\u30C4\u30A4\u30F3\u5B66\u7FD2R2R2R/\u30B5\u30FC\u30D3\u30B9\u30ED\u30DC\u30C3\u30C8\u5C0E\u5165\u30B7\u30DF\u30E5\u30EC\u30FC\u30B7\u30E7\u30F3/unity/screens"
screenshotWidth: 1920
screenshotHeight: 1080
Experiment/ExperimentPlan.cs
using System.Collections.Generic;
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 回す条件と、書き出す場所。実験に関する数字と場所はすべてこの .asset に置く。
/// 条件を順に回すのは ExperimentRunner(段階2で作る)。
/// </summary>
[CreateAssetMenu(menuName = "ServiceSim/Experiment Plan", fileName = "ExperimentPlan")]
public class ExperimentPlan : ScriptableObject
{
public struct Condition
{
public string id;
public int aisleWidthIndex;
public float aisleWidthM;
public CrowdLevel crowd;
public int robots;
}
[Header("この回の名前")]
public string runName = "family_v0";
[Header("回す条件")]
[Tooltip("細い通路の幅(StoreLayout.aisleWidthOptions の番号)")]
public int[] aisleWidthIndices = { 0, 1, 2 };
[Tooltip("混み具合")]
public CrowdLevel[] crowdLevels = { CrowdLevel.Empty, CrowdLevel.Normal, CrowdLevel.Busy };
[Tooltip("ロボットの台数")]
public int[] robotCounts = { 1, 2 };
[Header("1条件で回す量")]
[Tooltip("1条件・1つの種で回す往復の数。台数を合わせた合計")]
public int tripsPerCondition = 100;
[Tooltip("乱数の種")]
public int[] seeds = { 0, 1, 2, 3, 4 };
[Tooltip("時間の進みを速める倍率")]
public float timeScale = 1f;
[Header("書き出す場所")]
[Tooltip("KIOXIA がつながっているかを確かめる場所")]
public string kioxiaVolumePath = "/Volumes/KIOXIA";
[Tooltip("測ったデータの置き場所。この下に {YYYYMMDD}_{run_name}/ を作る")]
public string kioxiaOutputRoot = "/Volumes/KIOXIA/r2r2r-data/service_sim/unity";
[Header("段階2:1条件だけ試す")]
[Tooltip("細い通路の幅(StoreLayout.aisleWidthOptions の番号)")]
public int trialAisleWidthIndex = 1;
[Tooltip("混み具合")]
public CrowdLevel trialCrowd = CrowdLevel.Normal;
[Tooltip("ロボットの台数")]
public int trialRobots = 2;
[Tooltip("乱数の種")]
public int trialSeed = 0;
[Tooltip("回す往復の数(台数を合わせた合計)。timeScale 1 で20分程度に収まる数にする")]
public int trialTrips = 20;
[Tooltip("速めるときの倍率")]
public float trialTimeScaleFast = 10f;
[Tooltip("1コマの時間を固定して回すときの、1コマの長さ(秒)")]
public float trialCaptureDeltaTime = 0.02f;
[Tooltip("実時間でこれを超えたら打ち切る(秒)。保険")]
public float trialMaxWallSeconds = 2400f;
[Tooltip("段階2の置き場所。Plastic のワークスペースと git の外にする(測定データとして残さない)")]
public string trialOutputRoot = "/Users/shimazueiichi/ServiceSim_tmp";
[Header("v2:1回の実行の区切り")]
[Tooltip("1回の実行の長さ(店の中の分)。試算の peak_min と同じ")]
public float measureMinutes = 120f;
[Tooltip("慣らしの長さ(店の中の分)。この分は計測に数えない")]
public float warmupMinutes = 15f;
[Header("v2:速く回す")]
[Tooltip("段階3で使う1コマの長さ(秒)。0.05 で約24倍速")]
public float measureCaptureDeltaTime = 0.05f;
[Tooltip("確かめ用に使う1コマの長さ(秒)。検査1・2でこちらと比べる")]
public float referenceCaptureDeltaTime = 0.02f;
[Tooltip("回している間、カメラを切って描画を止める。計測を速くするため初期値は ON")]
public bool disableRenderingWhileRunning = true;
[Tooltip("回している間も頭の上の字幕を出す(B11 を一時的に外す)。" +
"人がロボットをよける様子を見て絵を撮るときだけ ON にする。計測は遅くなる")]
public bool showLabelsWhileRunning;
[Header("v2:段階3(18条件 × 種)")]
[Tooltip("段階3の置き場所。この下に {YYYYMMDD}_{run_name}/{condition}_s{seed}/ を作る")]
public string stage3OutputRoot = "/Users/shimazueiichi/ServiceSim_tmp/stage3";
[Tooltip("すでに書き出してある条件は飛ばす(途中から再開する)")]
public bool stage3Resume = true;
[Header("スクリーンショット")]
[Tooltip("段階1の報告に使う画面の置き場所")]
public string screenshotDir = "/Users/shimazueiichi/Library/Mobile Documents/com~apple~CloudDocs/デスクトップ/ADAPT DESIGN/じぶんの/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション/unity/screens";
public int screenshotWidth = 1920;
public int screenshotHeight = 1080;
/// <summary>条件の数(通路の幅 × 混み具合 × 台数)</summary>
public int ConditionCount
{
get
{
int w = aisleWidthIndices != null ? aisleWidthIndices.Length : 0;
int c = crowdLevels != null ? crowdLevels.Length : 0;
int r = robotCounts != null ? robotCounts.Length : 0;
return w * c * r;
}
}
/// <summary>18条件を順に並べる</summary>
public List<Condition> BuildConditions(StoreLayout layout)
{
List<Condition> list = new List<Condition>();
if (aisleWidthIndices == null || crowdLevels == null || robotCounts == null) return list;
for (int wi = 0; wi < aisleWidthIndices.Length; wi++)
{
int widthIndex = aisleWidthIndices[wi];
float widthM = 0f;
if (layout != null && layout.aisleWidthOptions != null &&
widthIndex >= 0 && widthIndex < layout.aisleWidthOptions.Length)
widthM = layout.aisleWidthOptions[widthIndex];
for (int ci = 0; ci < crowdLevels.Length; ci++)
{
for (int ri = 0; ri < robotCounts.Length; ri++)
{
Condition c = new Condition();
c.aisleWidthIndex = widthIndex;
c.aisleWidthM = widthM;
c.crowd = crowdLevels[ci];
c.robots = robotCounts[ri];
c.id = ConditionId(widthM, c.crowd, c.robots);
list.Add(c);
}
}
}
return list;
}
public static string ConditionId(float aisleWidthM, CrowdLevel crowd, int robots)
{
int mm = Mathf.RoundToInt(aisleWidthM * 100f);
return "w" + mm.ToString("000") + "_" + CrowdName(crowd) + "_r" + robots.ToString();
}
public static string CrowdName(CrowdLevel crowd)
{
switch (crowd)
{
case CrowdLevel.Empty: return "empty";
case CrowdLevel.Busy: return "busy";
default: return "normal";
}
}
}
}
Experiment/ExperimentRunner.cs
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// 条件を回して記録する。1回の Play の中で、条件を順に通せる(段階3の Run All)。
/// 数字と場所は ExperimentPlan の .asset に置く。回し方だけメニューが決める。
///
/// この script は実験だけを扱う。店の運営は ServiceOps、人は GroupManager に任せる。
/// </summary>
public class ExperimentRunner : MonoBehaviour
{
/// <summary>時間の進め方</summary>
public enum TimeMode { RealTime, FastTimeScale, FixedStep }
/// <summary>回す条件1つ</summary>
[System.Serializable]
public class Job
{
public int aisleWidthIndex;
public CrowdLevel crowd;
public int robots;
public int seed;
public string folder;
public string conditionId;
/// <summary>
/// 卓の停止点を通路の中心からずらす距離(m)。メニューが決める(C1)。
/// applyDeliveryOffset が false のときは、.asset の値をそのまま使う。
/// true のときも、使うのはこの回だけ。回し終わりに .asset の値へ戻すので、
/// .asset は書き換わらない(run_info.json の delivery_offset_used_m に実際の値が出る)。
/// </summary>
public bool applyDeliveryOffset;
public float deliveryOffset;
}
[Header("参照(Setup が入れる)")]
public ExperimentPlan plan;
public ServiceSpec service;
public StoreBuilder store;
public NavMeshBaker baker;
public PeopleSpawner people;
public ServiceOps ops;
public GroupManager groups;
public JamWatcher jams;
public RobotSpec robotSpec;
public GameObject robotPrefab;
public Transform robotsRoot;
[Header("回し方(メニューが決める)")]
public bool runOnPlay;
/// <summary>
/// 見る用の回か。字幕と描画をこの回だけ出し、時間は1倍速で進める。
/// 設定ファイル(ExperimentPlan)は書き換えない。計測の比べには使わない。
/// </summary>
public bool viewMode;
public TimeMode timeMode = TimeMode.FixedStep;
public string runLabel = "v2";
public float captureDeltaTimeOverride;
[Header("いまの様子(読むだけ)")]
[SerializeField] int _jobIndex;
[SerializeField] int _jobCount;
[SerializeField] float _simMinutes;
[SerializeField] float _wallSeconds;
[SerializeField] int _runsDone;
[SerializeField] bool _running;
// Play に入るとき、Unity はいちど script を読み直す(Domain Reload)。
// そのときに消えないように、回す条件は .unity に書き出して持たせる。
// メニューが入れた条件が Start() まで残らないと「回す条件がありません」で止まる。
[SerializeField] List<Job> _queue = new List<Job>();
readonly TripLogger _logger = new TripLogger();
readonly List<ServingRobotController> _robots = new List<ServingRobotController>();
readonly TripLogger.RunInfo _info = new TripLogger.RunInfo();
readonly List<int> _walkerSamples = new List<int>();
/// <summary>計測が始まってからこの回数走っても記録が0なら、取りこぼしとみて止める</summary>
const int TripRecordAlarmRuns = 3;
Job _job;
// 停止点は、メニューが決めた値をこの回だけ使う(C1)。
// .asset を書き換えたままにしないよう、もとの値を覚えて回し終わりに戻す。
float _savedDeliveryOffset;
bool _deliveryOffsetSaved;
float _endS, _measureStartS, _wallStart, _lastLog, _timeOffset;
int _tripSeq;
int _runsFinished; // ロボットが走り終わった回数(慣らしも含む)
int _runsAfterMeasure; // 計測が始まってから走り終わった回数
bool _stoppedEarly;
bool _jobRunning;
public bool Running { get { return _running; } }
/// <summary>回す条件を1つだけ入れる(段階2のやり方)</summary>
public void SetSingleJob(Job job)
{
_queue.Clear();
_queue.Add(job);
}
/// <summary>回す条件をまとめて入れる(段階3の Run All)</summary>
public void SetQueue(IList<Job> jobs)
{
_queue.Clear();
if (jobs != null) for (int i = 0; i < jobs.Count; i++) _queue.Add(jobs[i]);
}
void Start()
{
if (!runOnPlay) return;
if (!Resolve()) return;
if (_queue.Count == 0) { Debug.LogError("[ServiceSim] 回す条件がありません"); return; }
Application.runInBackground = true;
ApplyTimeMode();
// 見る用の回は、描画を止めない(字幕も ServiceSimLabels が出す)
if (plan.disableRenderingWhileRunning && !viewMode) DisableCameras();
_jobIndex = 0;
_jobCount = _queue.Count;
_runsDone = 0;
_wallStart = Time.realtimeSinceStartup;
_running = true;
StartJob(_queue[0]);
}
bool Resolve()
{
if (store == null) store = FindObjectOfType<StoreBuilder>();
if (baker == null) baker = FindObjectOfType<NavMeshBaker>();
if (people == null) people = FindObjectOfType<PeopleSpawner>();
if (ops == null) ops = FindObjectOfType<ServiceOps>();
if (groups == null) groups = FindObjectOfType<GroupManager>();
if (jams == null) jams = FindObjectOfType<JamWatcher>();
if (robotsRoot == null && store != null && store.transform.parent != null)
robotsRoot = store.transform.parent.Find("Robots");
if (plan == null || service == null || store == null || baker == null || people == null ||
ops == null || groups == null || jams == null || robotPrefab == null ||
robotsRoot == null || store.layout == null)
{
Debug.LogError("[ServiceSim] ExperimentRunner の参照が足りません。ServiceSim/Setup を実行してください");
return false;
}
return true;
}
void ApplyTimeMode()
{
Time.captureDeltaTime = 0f;
switch (timeMode)
{
case TimeMode.RealTime: Time.timeScale = 1f; break;
case TimeMode.FastTimeScale: Time.timeScale = plan.trialTimeScaleFast; break;
default:
Time.timeScale = 1f;
Time.captureDeltaTime = captureDeltaTimeOverride > 0f
? captureDeltaTimeOverride : plan.measureCaptureDeltaTime;
break;
}
}
void DisableCameras()
{
Camera[] cams = FindObjectsOfType<Camera>();
for (int i = 0; i < cams.Length; i++) cams[i].enabled = false;
}
// ---------------- 1条件ぶん ----------------
void StartJob(Job job)
{
_job = job;
_logger.Clear();
_info.frameDeltas.Clear();
_info.robotSteps.Clear();
_info.peopleSteps.Clear();
_info.chairPullsByReason.Clear();
_info.chairPulls = 0;
_walkerSamples.Clear();
_tripSeq = 0;
_runsFinished = 0;
_runsAfterMeasure = 0;
_stoppedEarly = false;
StoreLayout layout = store.layout;
layout.aisleWidthIndex = job.aisleWidthIndex;
// 停止点は条件の1つ(C1)。メニューが決めた値を、組み立てる前に入れる。
// .asset のもとの値は覚えておき、ぜんぶ終わったら戻す(.asset は書き換えない)。
if (!_deliveryOffsetSaved)
{
_savedDeliveryOffset = layout.deliveryOffsetFromAisleCenter;
_deliveryOffsetSaved = true;
}
layout.deliveryOffsetFromAisleCenter = job.applyDeliveryOffset
? job.deliveryOffset : _savedDeliveryOffset;
job.conditionId = ExperimentPlan.ConditionId(layout.AisleWidth, job.crowd, job.robots);
store.Build();
baker.Bake();
// 見る用の回は、床の塗り分けを Build Store と同じに作り直す。
// 計測の回では作らない(見た目だけのものなので、速さを落とさない)
if (viewMode)
{
store.PaintFloor(baker, people.spec != null ? people.spec.personRadius : 0.2f);
PlaceNameLabels.Rebuild(store); // 場所の名前も出す
}
else
{
PlaceNameLabels.Clear(store); // 計測の回では出さない
}
ops.spec = service;
ops.robotSpec = robotSpec;
ops.store = store;
ops.Setup();
people.service = service;
people.ops = ops;
people.Spawn(job.seed);
groups.spec = service;
groups.people = people.spec;
groups.ops = ops;
groups.store = store;
groups.spawner = people; // 人の動きに合わせて椅子を引く(B6)
groups.bodyRoot = transform;
jams.spec = people.spec;
jams.ops = ops;
jams.store = store;
jams.Setup();
SpawnRobots(job.robots);
groups.Setup(job.crowd, job.seed, people.Targets);
_measureStartS = plan.warmupMinutes * 60f;
_endS = (plan.warmupMinutes + plan.measureMinutes) * 60f;
_timeOffset = Time.time;
_lastLog = 0f;
_jobRunning = true;
Debug.Log("[ServiceSim] 回し始め " + (_jobIndex + 1) + "/" + _jobCount +
":" + job.conditionId + " 種" + job.seed +
"(慣らし " + plan.warmupMinutes + " 分 + 計測 " + plan.measureMinutes + " 分)");
}
void SpawnRobots(int count)
{
ops.ClearRobots();
for (int i = robotsRoot.childCount - 1; i >= 0; i--) Destroy(robotsRoot.GetChild(i).gameObject);
_robots.Clear();
Vector3 pass = store.PassPoint;
for (int i = 0; i < count; i++)
{
GameObject go = Instantiate(robotPrefab, robotsRoot);
go.name = "ServingRobot" + (i + 1).ToString("00");
ServingRobotController c = go.GetComponent<ServingRobotController>();
if (c == null) { Destroy(go); continue; }
c.robotId = i + 1;
c.PlaceAt(pass + new Vector3(0f, 0f, -0.8f * i), Quaternion.LookRotation(Vector3.back, Vector3.up));
c.TripCompleted += OnRunDone;
_robots.Add(c);
ops.RegisterRobot(c);
}
}
void Update()
{
if (!_running || !_jobRunning) return;
float dt = Time.deltaTime;
_wallSeconds = Time.realtimeSinceStartup - _wallStart;
_info.frameDeltas.Add(dt);
float step = 0f;
for (int i = 0; i < _robots.Count; i++)
if (_robots[i] != null) step = Mathf.Max(step, _robots[i].LastStepM);
_info.robotSteps.Add(step);
// 人の1コマの移動量も記録する(B10)。通路を歩く人のうちいちばん大きいもの
float peopleStep = 0f;
IReadOnlyList<PersonBody> bodies = PersonBody.All;
for (int i = 0; i < bodies.Count; i++)
{
PersonBody b = bodies[i];
if (b == null || !b.walksInAisle) continue;
if (b.LastStepM > peopleStep) peopleStep = b.LastStepM;
}
_info.peopleSteps.Add(peopleStep);
ops.Advance(dt);
groups.Tick(dt);
jams.Tick(dt);
_simMinutes = ops.NowS / 60f;
if (ops.NowS >= _measureStartS) _walkerSamples.Add(groups.WalkingBodies + people.StaffSpawned);
if (_wallSeconds - _lastLog >= 60f)
{
_lastLog = _wallSeconds;
// 「走り」はロボットが走り終わった回数、「記録」は trips.csv に書いた行数。
// 走りがあるのに記録が増えないときは、記録の取りこぼしなのでその場で止める
Debug.Log("[ServiceSim] とちゅう " + (_jobIndex + 1) + "/" + _jobCount + ":" +
_simMinutes.ToString("0.0") + " / " + (_endS / 60f).ToString("0") + " 分、" +
"組 " + groups.GroupsArrived + "(待ち " + groups.WaitingGroups + ")、" +
"走り " + _runsFinished + " 回・記録 " + _logger.RowCount + " 行、" +
"椅子 " + ops.chairPulls + " 回、" +
"あきらめ " + (ops.gaveUpCount + ops.warpedCount + ops.tableFreedCount) + " 回、" +
"実時間 " + (_wallSeconds / 60f).ToString("0.0") + " 分");
CheckTripsAreRecorded();
}
if (ops.NowS >= _endS) FinishJob();
}
/// <summary>
/// 走りがあるのに trips.csv の行が増えないときは、記録の取りこぼし。
/// 気づかないまま何時間も回さないように、その場で止めて知らせる。
/// 慣らしの間に始まった走りは数えないので、計測が始まってから見る。
/// </summary>
void CheckTripsAreRecorded()
{
if (ops.NowS < _measureStartS) return;
if (_runsAfterMeasure <= 0) return;
if (_logger.RowCount > 0) return;
// 計測が始まってから走り終わった回数がこれを超えても記録が0なら、おかしい
if (_runsAfterMeasure < TripRecordAlarmRuns) return;
Debug.LogError("[ServiceSim] 走りが " + _runsAfterMeasure +
" 回あるのに trips.csv の行が 0 です。記録を取りこぼしています。" +
"\n 止めました。ExperimentRunner.OnRunDone と " +
"ServingRobotController.RunStartS を確かめてください" +
"\n 店の中 " + (ops.NowS / 60f).ToString("0.0") + " 分 / 計測の始まり " +
(_measureStartS / 60f).ToString("0.0") + " 分");
StopEarly();
}
/// <summary>途中で止める(書き出しはする)</summary>
void StopEarly()
{
_stoppedEarly = true;
FinishJob();
AllDone();
}
void OnRunDone(ServingRobotController r)
{
_runsFinished++;
if (ops.NowS >= _measureStartS) _runsAfterMeasure++;
// 走り始めた時刻はロボットが持っている(ServingRobotController.RunStartS)。
// ここで覚えておくと、走り終わってすぐ次の仕事が入ったとき、
// 「空いている」状態を一度も見ないまま時刻が古いままになり、全部の走りを取りこぼす。
float startS = r.RunStartS >= 0f ? r.RunStartS : ops.NowS - r.TotalS;
if (startS < _measureStartS) return;
IList<DishOrder> batch = ops.LastBatchOf(r);
_tripSeq++;
TripLogger.Row row = new TripLogger.Row();
row.conditionId = _job.conditionId;
row.aisleWidthM = store.layout.AisleWidth;
row.crowd = ExperimentPlan.CrowdName(_job.crowd);
row.robots = _job.robots;
row.seed = _job.seed;
row.robotId = r.robotId;
row.tripId = _tripSeq;
row.tableId = r.TableIds.Count > 0 ? r.TableIds[0] : 0;
row.distanceM = r.DistanceM;
row.totalS = r.TotalS;
row.stoppedS = r.StoppedS;
row.nStops = r.StopCount;
row.stopDurationsS = ToArray(r.StopDurationsS);
row.stuck = r.Stuck;
row.timeout = r.TimedOut;
row.waitAisleS = r.WaitAisleS;
row.maxDecelMps2 = r.MaxDecelMps2;
row.minClearanceM = r.MinClearanceM;
row.metRobot = r.MetRobot;
int[] ids = new int[r.TableIds.Count];
for (int i = 0; i < ids.Length; i++) ids[i] = r.TableIds[i];
row.tableIds = ids;
row.dishes = batch.Count;
float[] ready = new float[batch.Count], arrived = new float[batch.Count];
int undelivered = 0;
for (int i = 0; i < batch.Count; i++)
{
ready[i] = batch[i].readyS;
arrived[i] = batch[i].arrivedS;
if (batch[i].arrivedS < 0f) undelivered++;
}
row.dishReadyS = ready;
row.dishArrivedS = arrived;
row.undelivered = undelivered;
_logger.Add(row);
AddStopRows(r, _tripSeq);
}
void AddStopRows(ServingRobotController robot, int tripId)
{
IReadOnlyList<ServingRobotController.StopEvent> events = robot.StopEvents;
for (int i = 0; i < events.Count; i++)
{
ServingRobotController.StopEvent e = events[i];
TripLogger.StopRow s = new TripLogger.StopRow();
s.conditionId = _job.conditionId;
s.seed = _job.seed;
s.robotId = robot.robotId;
s.tripId = tripId;
s.tS = e.startTimeS - _timeOffset;
s.x = e.position.x;
s.z = e.position.z;
s.aisle = AisleNameAt(e.position);
s.cause = TripLogger.CauseLabel(e.cause);
s.causeDetail = e.causeDetail;
s.pathBlocked = e.pathBlocked;
s.durationS = e.durationS;
s.resolved = TripLogger.ResolveLabel(e.resolve);
s.clearanceLeftCm = e.clearanceLeftM * 100f;
s.clearanceRightCm = e.clearanceRightM * 100f;
s.minGapCm = Mathf.Min(s.clearanceLeftCm, s.clearanceRightCm);
// 何で動けなかったかの内訳(B7)
s.blockedS = e.blockedS;
s.noPathS = e.noPathS;
s.yieldS = e.yieldS;
s.clearWaitS = e.clearWaitS;
s.replans = e.replans;
_logger.AddStop(s);
}
}
string AisleNameAt(Vector3 p)
{
string wide = "";
IList<StoreBuilder.AisleInfo> aisles = store.Aisles;
for (int i = 0; i < aisles.Count; i++)
{
Bounds b = aisles[i].zone;
if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue;
if (aisles[i].narrow) return aisles[i].name;
if (wide.Length == 0) wide = aisles[i].name;
}
return wide;
}
static float[] ToArray(IReadOnlyList<float> list)
{
if (list == null || list.Count == 0) return new float[0];
float[] a = new float[list.Count];
for (int i = 0; i < a.Length; i++) a[i] = list[i];
return a;
}
// ---------------- 書き出して次へ ----------------
void FinishJob()
{
_jobRunning = false;
AddGroupRows();
string dir = _job.folder;
try
{
Directory.CreateDirectory(dir);
TripLogger.SimLike sim = new TripLogger.SimLike();
sim.dishWaitMin.AddRange(ops.dishWaitMin);
sim.seatWaitMin.AddRange(ops.seatWaitMin);
sim.turnMin.AddRange(ops.turnMin);
sim.coldMin = service.coldMin;
sim.robotDishes = ops.robotDishes;
sim.staffDishes = ops.staffDishes;
sim.requeuedDishes = ops.requeuedDishes;
sim.groups = groups.GroupsDone;
sim.jamCount = jams.JamCount;
sim.jamTotalS = jams.TotalJamSeconds;
sim.robotBlockedJams = jams.RobotBlockedJamCount; // B9
sim.chairPulls = ops.chairPulls; // B6
sim.robotContacts = ops.Contacts.Count; // 人がロボットをよける
sim.robotYieldWaits = ops.robotYieldWaits;
sim.robotYieldWaitSeconds = ops.robotYieldWaitSeconds;
sim.robotYieldDetours = ops.robotYieldDetours;
sim.registerCrowdMax = ops.registerCrowdMax; // レジの前の人数
sim.registerCrowdMean = ops.RegisterCrowdMean;
sim.registerPayCount = ops.registerPayCount;
sim.registerPayNotAtBackCount = ops.registerPayNotAtBackCount;
sim.registerPayOffSpotCount = ops.registerPayOffSpotCount;
sim.registerPayMeanOffsetM = ops.RegisterPayMeanOffsetM;
sim.registerPayMaxOffsetM = ops.registerPayMaxOffsetM;
sim.registerBackIntruderCount = ops.registerBackIntruderCount;
sim.registerBackIntruderSeconds = ops.registerBackIntruderSeconds;
sim.staffTime.Clear();
foreach (KeyValuePair<string, float> kv in ops.StaffTime) sim.staffTime.Add(kv);
sim.dishParts.Clear();
sim.dishParts.AddRange(ops.DishPartsList);
sim.jobWaitMean.Clear();
foreach (KeyValuePair<string, float> kv in ops.JobWaitSum)
{
int n;
ops.JobWaitCount.TryGetValue(kv.Key, out n);
sim.jobWaitMean.Add(new KeyValuePair<string, float>(kv.Key, n > 0 ? kv.Value / n : 0f));
}
sim.drinkBarQueued = ops.drinkBarQueued; // B4
sim.drinkBarGaveUp = ops.drinkBarGaveUp;
sim.drinkBarWaitMin.Clear();
sim.drinkBarWaitMin.AddRange(ops.drinkBarWaitMin);
sim.gaveUp = ops.gaveUpCount; // B1
sim.warped = ops.warpedCount;
sim.tableFreed = ops.tableFreedCount;
sim.walkersMean = MeanInt(_walkerSamples);
sim.measuredMinutes = plan.measureMinutes;
_logger.SetSimLike(sim);
_logger.WriteTripsCsv(Path.Combine(dir, "trips.csv"));
_logger.WriteStopsCsv(Path.Combine(dir, "stops.csv"));
_logger.WriteGroupsCsv(Path.Combine(dir, "groups.csv"));
TripLogger.WriteJamsCsv(Path.Combine(dir, "people_jams.csv"),
_job.conditionId, _job.seed, jams.Rows);
TripLogger.WriteGiveUpsCsv(Path.Combine(dir, "give_ups.csv"),
_job.conditionId, _job.seed, ops.GiveUps);
TripLogger.WriteRequeuesCsv(Path.Combine(dir, "dishes.csv"),
_job.conditionId, _job.seed, ops.Requeues);
TripLogger.WriteContactsCsv(Path.Combine(dir, "contacts.csv"),
_job.conditionId, _job.seed, ops.Contacts);
_logger.WriteSummaryJson(Path.Combine(dir, "summary.json"), plan.runName);
_info.runLabel = runLabel;
_info.conditionId = _job.conditionId;
_info.timeMode = timeMode.ToString();
_info.tripsRequested = 0;
_info.tripsDone = _logger.RowCount;
_info.robots = _job.robots;
_info.seed = _job.seed;
_info.aisleWidthM = store.layout.AisleWidth;
_info.deliveryOffsetM = store.layout.deliveryOffsetFromAisleCenter;
_info.deliveryOffsetUsedM = store.layout.DeliveryOffset();
_info.crowd = ExperimentPlan.CrowdName(_job.crowd);
_info.simSeconds = ops.NowS;
_info.wallSeconds = _wallSeconds;
_info.timeScale = timeMode == TimeMode.FastTimeScale ? plan.trialTimeScaleFast : 1f;
_info.captureDeltaTime = Time.captureDeltaTime;
// 椅子を引いた回数(B6)
_info.chairPulls = ops.chairPulls;
_info.chairPullsByReason.Clear();
foreach (KeyValuePair<string, int> kv in ops.ChairPullsByReason)
_info.chairPullsByReason.Add(kv);
_info.stoppedEarly = _stoppedEarly;
_info.storeKind = store.layout.name;
TripLogger.WriteRunInfoJson(Path.Combine(dir, "run_info.json"), _info);
TripLogger.WriteConditionsJson(Path.Combine(dir, "conditions.json"),
plan.runName, store.layout, robotSpec, people.spec, plan,
plan.BuildConditions(store.layout));
_runsDone++;
Debug.Log("[ServiceSim] 書き出し " + (_jobIndex + 1) + "/" + _jobCount + ":" + _job.conditionId +
" 種" + _job.seed +
"\n 走り " + _runsFinished + " 回 / 記録 " + _logger.RowCount + " 行 / 止まり " +
_logger.StopCount + " 回 / 組 " + _logger.GroupRowCount +
"\n 椅子を引いた " + ops.chairPulls + " 回 / 人どうしの詰まり " + jams.JamCount +
" 回(うちロボットが近くで止まっていた " + jams.RobotBlockedJamCount + " 回)" +
"\n 会計 " + ops.registerPayCount + " 回(店員側にいなかった " +
ops.registerPayNotAtBackCount + " 回・立つ点から離れた " +
ops.registerPayOffSpotCount + " 回・いちばん離れた " +
ops.registerPayMaxOffsetM.ToString("0.00") + " m・会計でないのに店員側にいた " +
ops.registerBackIntruderCount + " 回)/ レジの前の人数 最大 " +
ops.registerCrowdMax + "・平均 " + ops.RegisterCrowdMean.ToString("0.0") +
"\n 人がロボットを待った " + ops.robotYieldWaits + " 回・" +
ops.robotYieldWaitSeconds.ToString("0") + " 秒 / 回り道 " +
ops.robotYieldDetours + " 回 / 近づきすぎ " + ops.Contacts.Count + " 回" +
"\n あきらめた " + ops.gaveUpCount + " / 移した " + ops.warpedCount +
" / 卓を空けた " + ops.tableFreedCount +
" / 戻した皿 " + ops.requeuedDishes + "(届いた " + ops.robotDishes + ")" +
"\n " + TripLogger.FrameStatsText(_info) +
"\n " + dir);
}
catch (System.Exception e)
{
Debug.LogError("[ServiceSim] 書き出しに失敗: " + dir + "\n" + e);
}
_jobIndex++;
if (_jobIndex < _queue.Count) { StartJob(_queue[_jobIndex]); return; }
AllDone();
}
void AddGroupRows()
{
IList<GroupManager.Group> gs = groups.Groups;
for (int i = 0; i < gs.Count; i++)
{
GroupManager.Group g = gs[i];
TripLogger.GroupRow r = new TripLogger.GroupRow();
r.conditionId = _job.conditionId;
r.seed = _job.seed;
r.groupId = g.id;
r.size = g.size;
r.tableId = g.table != null ? g.table.tableId : 0;
r.tableSeats = g.table != null ? g.table.seatCount : 0; // 2人席/4人席
r.arriveS = g.arriveS;
r.guideStartS = g.guideStartS;
r.seatedS = g.seatedS;
r.eatEndS = g.eatEndS;
r.payStartS = g.payStartS;
r.payS = g.payS;
r.leftS = g.leftS;
r.waitedMin = g.seatedS >= 0f && g.arriveS >= 0f ? (g.seatedS - g.arriveS) / 60f : -1f;
r.warmup = g.warmup;
r.state = g.state.ToString();
_logger.AddGroup(r);
}
}
static float MeanInt(List<int> v)
{
if (v == null || v.Count == 0) return 0f;
long s = 0;
for (int i = 0; i < v.Count; i++) s += v[i];
return (float)s / v.Count;
}
/// <summary>
/// メニューが決めた停止点を、.asset のもとの値へ戻す(C1)。
/// 書き出しが終わったあとに呼ぶ。.asset を書き換えたままにしないため。
/// </summary>
void RestoreDeliveryOffset()
{
if (!_deliveryOffsetSaved) return;
_deliveryOffsetSaved = false;
if (store != null && store.layout != null)
store.layout.deliveryOffsetFromAisleCenter = _savedDeliveryOffset;
}
void AllDone()
{
_running = false;
RestoreDeliveryOffset();
Time.captureDeltaTime = 0f;
Time.timeScale = 1f;
Debug.Log("[ServiceSim] ぜんぶ終わりました。" + _runsDone + " / " + _jobCount + " 条件、" +
"実時間 " + (_wallSeconds / 60f).ToString("0.0") + " 分");
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#endif
}
void OnDisable()
{
// 途中で Play を抜けたときも、停止点を .asset のもとの値へ戻す
RestoreDeliveryOffset();
Time.captureDeltaTime = 0f;
if (_running) Time.timeScale = 1f;
}
}
}
Experiment/TripLogger.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEngine;
namespace ServiceSim
{
/// <summary>
/// trips.csv・conditions.json・summary.json を書く。
/// この script は実験だけを扱う。店・ロボット・人は動かさない。
/// 1往復ぶんの値は ExperimentRunner が Row に詰めて渡す。
/// </summary>
public class TripLogger
{
/// <summary>trips.csv の1行</summary>
public struct Row
{
public string conditionId;
public float aisleWidthM;
public string crowd;
public int robots;
public int seed;
public int robotId;
public int tripId;
public int tableId;
public float distanceM;
public float totalS;
public float stoppedS;
public int nStops;
public float[] stopDurationsS;
public bool stuck;
public bool timeout;
public float waitAisleS;
public float maxDecelMps2;
public float minClearanceM;
public bool metRobot;
// v2:1行=ロボットの1走り
public int[] tableIds; // 回った卓
public int dishes; // 載せた料理の数
public float[] dishReadyS; // 各料理ができた時刻
public float[] dishArrivedS; // 各料理が届いた時刻(-1 は届かなかった)
public int undelivered; // 届けられず受け渡し口に戻した数
}
/// <summary>groups.csv の1行(1組ぶん)</summary>
public struct GroupRow
{
public string conditionId;
public int seed;
public int groupId;
public int size;
public int tableId;
/// <summary>座った卓の大きさ(2 なら2人席、4 なら4人席。0 は座れなかった)</summary>
public int tableSeats;
public float arriveS, guideStartS, seatedS, eatEndS, payS, leftS;
/// <summary>会計を始めた時刻(食べ終わって会計待ちに入った時刻, 秒)</summary>
public float payStartS;
public float waitedMin;
public bool warmup;
public string state;
}
/// <summary>stops.csv の1行(止まり1回ぶん)</summary>
public struct StopRow
{
public string conditionId;
public int seed;
public int robotId;
public int tripId;
public float tS; // 店の中の時刻
public float x, z;
public string aisle; // 通路の名前
public string cause;
public string causeDetail;
public bool pathBlocked; // 行き先への道が引けていなかったか
public float durationS;
public string resolved;
public float clearanceLeftCm;
public float clearanceRightCm;
public float minGapCm;
// 止まっている間、何で動けなかったかの内訳(秒)。B7 を調べるため
public float blockedS;
public float noPathS;
public float yieldS;
public float clearWaitS;
public int replans;
}
/// <summary>
/// 止まった原因の名前。観察シート(serving-robot-observation.html)の
/// 「止まった原因」の選択肢にそろえる。Unity にしかない原因は足した。
/// </summary>
public static string CauseLabel(ServingRobotController.StopCause cause)
{
switch (cause)
{
case ServingRobotController.StopCause.Guest: return "お客さんとすれ違った";
case ServingRobotController.StopCause.Staff: return "スタッフとすれ違った";
case ServingRobotController.StopCause.PersonStanding: return "人が立ち止まっていた";
case ServingRobotController.StopCause.Chair: return "椅子・荷物・ベビーカー";
case ServingRobotController.StopCause.NoPath: return "通路の幅が足りない";
case ServingRobotController.StopCause.OtherRobot: return "もう1台のロボット";
case ServingRobotController.StopCause.AisleQueue: return "通路の順番待ち";
default: return "分からない";
}
}
/// <summary>どう動き出したかの名前</summary>
public static string ResolveLabel(ServingRobotController.StopResolve r)
{
switch (r)
{
case ServingRobotController.StopResolve.SelfResume: return "自分で動き出した";
case ServingRobotController.StopResolve.Replanned: return "経路を引き直した";
case ServingRobotController.StopResolve.Stuck: return "動けなくなった";
default: return "入口で待った";
}
}
readonly List<Row> _rows = new List<Row>();
readonly List<StopRow> _stops = new List<StopRow>();
public int StopCount { get { return _stops.Count; } }
public IReadOnlyList<StopRow> Stops { get { return _stops; } }
public void AddStop(StopRow row) { _stops.Add(row); }
public int RowCount { get { return _rows.Count; } }
public IReadOnlyList<Row> Rows { get { return _rows; } }
public void Add(Row row)
{
_rows.Add(row);
}
public void Clear()
{
_rows.Clear();
_stops.Clear();
_groups.Clear();
}
// ---------------- trips.csv ----------------
public const string Header =
"condition_id,aisle_width_m,crowd,robots,seed,robot_id,trip_id,table_id," +
"distance_m,total_s,stopped_s,n_stops,stop_durations_s,stuck,timeout,wait_aisle_s," +
"max_decel_mps2,min_clearance_m,met_robot," +
"table_ids,dishes,dish_ready_s,dish_arrived_s,undelivered";
public void WriteTripsCsv(string path)
{
StringBuilder sb = new StringBuilder();
sb.Append(Header).Append('\n');
for (int i = 0; i < _rows.Count; i++) AppendRow(sb, _rows[i]);
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
static void AppendRow(StringBuilder sb, Row r)
{
sb.Append(r.conditionId).Append(',')
.Append(F(r.aisleWidthM)).Append(',')
.Append(r.crowd).Append(',')
.Append(r.robots).Append(',')
.Append(r.seed).Append(',')
.Append(r.robotId).Append(',')
.Append(r.tripId).Append(',')
.Append(r.tableId).Append(',')
.Append(F(r.distanceM)).Append(',')
.Append(F(r.totalS)).Append(',')
.Append(F(r.stoppedS)).Append(',')
.Append(r.nStops).Append(',')
.Append(Join(r.stopDurationsS)).Append(',')
.Append(r.stuck ? 1 : 0).Append(',')
.Append(r.timeout ? 1 : 0).Append(',')
.Append(F(r.waitAisleS)).Append(',')
.Append(F(r.maxDecelMps2)).Append(',')
.Append(F(r.minClearanceM)).Append(',')
.Append(r.metRobot ? 1 : 0).Append(',')
.Append(JoinInt(r.tableIds)).Append(',')
.Append(r.dishes).Append(',')
.Append(Join(r.dishReadyS)).Append(',')
.Append(Join(r.dishArrivedS)).Append(',')
.Append(r.undelivered)
.Append('\n');
}
static string Join(float[] values)
{
if (values == null || values.Length == 0) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < values.Length; i++)
{
if (i > 0) sb.Append(';');
sb.Append(F(values[i]));
}
return sb.ToString();
}
static string JoinInt(int[] values)
{
if (values == null || values.Length == 0) return "";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < values.Length; i++)
{
if (i > 0) sb.Append(';');
sb.Append(values[i]);
}
return sb.ToString();
}
static string F(float v)
{
return v.ToString("0.####", CultureInfo.InvariantCulture);
}
// ---------------- groups.csv ----------------
readonly List<GroupRow> _groups = new List<GroupRow>();
public int GroupRowCount { get { return _groups.Count; } }
public void AddGroup(GroupRow r) { _groups.Add(r); }
/// <summary>table_seats は座った卓の大きさ(2=2人席・4=4人席・0=座れなかった)</summary>
public const string GroupHeader =
"condition_id,seed,group_id,size,table_id,table_seats,arrive_s,guide_start_s,seated_s," +
"eat_end_s,pay_start_s,pay_s,left_s,waited_min,warmup,state";
public void WriteGroupsCsv(string path)
{
StringBuilder sb = new StringBuilder();
sb.Append(GroupHeader).Append('\n');
for (int i = 0; i < _groups.Count; i++)
{
GroupRow r = _groups[i];
sb.Append(r.conditionId).Append(',').Append(r.seed).Append(',')
.Append(r.groupId).Append(',').Append(r.size).Append(',').Append(r.tableId).Append(',')
.Append(r.tableSeats).Append(',')
.Append(F(r.arriveS)).Append(',').Append(F(r.guideStartS)).Append(',')
.Append(F(r.seatedS)).Append(',').Append(F(r.eatEndS)).Append(',')
.Append(F(r.payStartS)).Append(',')
.Append(F(r.payS)).Append(',').Append(F(r.leftS)).Append(',')
.Append(F(r.waitedMin)).Append(',').Append(r.warmup ? 1 : 0).Append(',')
.Append(r.state).Append('\n');
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- people_jams.csv ----------------
public const string JamHeader =
"condition_id,seed,t_s,x,z,aisle,people,duration_s,resolved,robot_blocked,robot_id";
public static void WriteJamsCsv(string path, string conditionId, int seed,
IReadOnlyList<JamWatcher.JamRow> rows)
{
StringBuilder sb = new StringBuilder();
sb.Append(JamHeader).Append('\n');
if (rows != null)
{
for (int i = 0; i < rows.Count; i++)
{
JamWatcher.JamRow r = rows[i];
sb.Append(conditionId).Append(',').Append(seed).Append(',')
.Append(F(r.tS)).Append(',').Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
.Append(r.aisle).Append(',').Append(r.people).Append(',')
.Append(F(r.durationS)).Append(',').Append(r.resolved).Append(',')
.Append(r.robotBlocked ? 1 : 0).Append(',').Append(r.robotId).Append('\n');
}
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- give_ups.csv(B1)----------------
public const string GiveUpHeader =
"condition_id,seed,t_s,who,job,table_id,to_x,to_z,x,z,aisle,waited_s,action";
/// <summary>あきらめた・移した・卓を空けたことを1回1行で書く</summary>
public static void WriteGiveUpsCsv(string path, string conditionId, int seed,
IReadOnlyList<GiveUpRow> rows)
{
StringBuilder sb = new StringBuilder();
sb.Append(GiveUpHeader).Append('\n');
if (rows != null)
{
for (int i = 0; i < rows.Count; i++)
{
GiveUpRow r = rows[i];
sb.Append(conditionId).Append(',').Append(seed).Append(',')
.Append(F(r.tS)).Append(',').Append(r.who).Append(',').Append(r.job).Append(',')
.Append(r.tableId).Append(',')
.Append(F(r.toX)).Append(',').Append(F(r.toZ)).Append(',')
.Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
.Append(r.aisle).Append(',').Append(F(r.waitedS)).Append(',')
.Append(r.action).Append('\n');
}
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- contacts.csv(人がロボットに近づきすぎた)----------------
public const string ContactHeader =
"condition_id,seed,t_s,x,z,aisle,place,who,careless,robot_id,gap_cm";
/// <summary>人がロボットの外周の近くまで来た1回を1行で書く</summary>
public static void WriteContactsCsv(string path, string conditionId, int seed,
IReadOnlyList<ContactRow> rows)
{
StringBuilder sb = new StringBuilder();
sb.Append(ContactHeader).Append('\n');
if (rows != null)
{
for (int i = 0; i < rows.Count; i++)
{
ContactRow r = rows[i];
sb.Append(conditionId).Append(',').Append(seed).Append(',')
.Append(F(r.tS)).Append(',').Append(F(r.x)).Append(',').Append(F(r.z)).Append(',')
.Append(r.aisle).Append(',').Append(r.place).Append(',')
.Append(r.who).Append(',')
.Append(r.careless ? 1 : 0).Append(',').Append(r.robotId).Append(',')
.Append(F(r.gapM * 100f)).Append('\n');
}
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- dishes.csv(B8)----------------
public const string RequeueHeader =
"condition_id,seed,t_s,dish_id,table_id,robot_id,fragile,ready_s,waited_s,requeued_so_far,why";
/// <summary>届けられずに受け渡し口へ戻した1回を1行で書く</summary>
public static void WriteRequeuesCsv(string path, string conditionId, int seed,
IReadOnlyList<RequeueRow> rows)
{
StringBuilder sb = new StringBuilder();
sb.Append(RequeueHeader).Append('\n');
if (rows != null)
{
for (int i = 0; i < rows.Count; i++)
{
RequeueRow r = rows[i];
sb.Append(conditionId).Append(',').Append(seed).Append(',')
.Append(F(r.tS)).Append(',').Append(r.dishId).Append(',').Append(r.tableId).Append(',')
.Append(r.robotId).Append(',').Append(r.fragile ? 1 : 0).Append(',')
.Append(F(r.readyS)).Append(',').Append(F(r.tS - r.readyS)).Append(',')
.Append(r.requeuedSoFar).Append(',').Append(r.why).Append('\n');
}
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- stops.csv ----------------
public const string StopHeader =
"condition_id,seed,robot_id,trip_id,t_s,x,z,aisle,cause,cause_detail,path_blocked," +
"duration_s,resolved,clearance_left_cm,clearance_right_cm,min_gap_cm," +
"blocked_s,no_path_s,yield_s,clear_wait_s,replans";
public void WriteStopsCsv(string path)
{
StringBuilder sb = new StringBuilder();
sb.Append(StopHeader).Append('\n');
for (int i = 0; i < _stops.Count; i++)
{
StopRow r = _stops[i];
sb.Append(r.conditionId).Append(',')
.Append(r.seed).Append(',')
.Append(r.robotId).Append(',')
.Append(r.tripId).Append(',')
.Append(F(r.tS)).Append(',')
.Append(F(r.x)).Append(',')
.Append(F(r.z)).Append(',')
.Append(r.aisle).Append(',')
.Append(r.cause).Append(',')
.Append(r.causeDetail).Append(',')
.Append(r.pathBlocked ? 1 : 0).Append(',')
.Append(F(r.durationS)).Append(',')
.Append(r.resolved).Append(',')
.Append(F(r.clearanceLeftCm)).Append(',')
.Append(F(r.clearanceRightCm)).Append(',')
.Append(F(r.minGapCm)).Append(',')
.Append(F(r.blockedS)).Append(',')
.Append(F(r.noPathS)).Append(',')
.Append(F(r.yieldS)).Append(',')
.Append(F(r.clearWaitS)).Append(',')
.Append(r.replans)
.Append('\n');
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
// ---------------- run_info.json(1回ぶんの回し方とコマの粗さ)----------------
/// <summary>1回ぶんの回し方と、コマの粗さの記録</summary>
public class RunInfo
{
public string runLabel = "";
public string conditionId = "";
public string timeMode = "";
public string storeKind = "";
public string crowd = "";
public int tripsRequested;
public int tripsDone;
public int robots;
public int seed;
public float aisleWidthM;
/// <summary>卓の停止点を通路の中心からずらす距離(m)。.asset に入れた値(C1)</summary>
public float deliveryOffsetM;
/// <summary>そのうち、実際に使った分(通路が狭いときは NavMesh に乗る範囲に抑える)</summary>
public float deliveryOffsetUsedM;
public float simSeconds;
public float wallSeconds;
public float timeScale;
public float captureDeltaTime;
public bool stoppedEarly;
/// <summary>1コマの時間(秒)。Time.deltaTime をそのまま</summary>
public readonly List<float> frameDeltas = new List<float>();
/// <summary>1コマでロボットが進んだ距離(m)。台のうちいちばん大きいもの</summary>
public readonly List<float> robotSteps = new List<float>();
/// <summary>
/// 1コマで人が進んだ距離(m)。通路を歩く人のうちいちばん大きいもの(B10)。
/// スタッフは 1.0 m/秒 なので、1コマ 0.05 秒だと 0.05 m でちょうど境目になる。
/// </summary>
public readonly List<float> peopleSteps = new List<float>();
/// <summary>椅子を引いた回数(B6)</summary>
public int chairPulls;
/// <summary>椅子を引いたきっかけごとの回数(B6)</summary>
public readonly List<KeyValuePair<string, int>> chairPullsByReason =
new List<KeyValuePair<string, int>>();
}
/// <summary>コマの粗さを1行にまとめる(Console 用)</summary>
public static string FrameStatsText(RunInfo info)
{
if (info == null || info.frameDeltas.Count == 0) return "コマの記録なし";
int over = 0;
for (int i = 0; i < info.robotSteps.Count; i++) if (info.robotSteps[i] > 0.05f) over++;
return "コマ " + info.frameDeltas.Count +
" / 1コマの時間 中央 " + F(Percentile(info.frameDeltas, 0.5f)) +
"s・90% " + F(Percentile(info.frameDeltas, 0.9f)) +
"s・最大 " + F(Percentile(info.frameDeltas, 1f)) + "s" +
" / 1コマで進む距離 最大 " + F(Percentile(info.robotSteps, 1f)) +
"m・90% " + F(Percentile(info.robotSteps, 0.9f)) + "m" +
" / 0.05 m を超えたコマ ロボット " + over + "(" +
(info.robotSteps.Count > 0 ? (100f * over / info.robotSteps.Count).ToString("0.0") : "0") +
"%)・人 " + CountOver(info.peopleSteps, 0.05f) + "(" +
(info.peopleSteps.Count > 0
? (100f * CountOver(info.peopleSteps, 0.05f) / info.peopleSteps.Count).ToString("0.0")
: "0") + "%)";
}
public static void WriteRunInfoJson(string path, RunInfo info)
{
StringBuilder sb = new StringBuilder();
sb.Append("{\n");
sb.Append(" \"run_label\": ").Append(Str(info.runLabel)).Append(",\n");
sb.Append(" \"written_at\": ").Append(Str(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))).Append(",\n");
sb.Append(" \"condition_id\": ").Append(Str(info.conditionId)).Append(",\n");
sb.Append(" \"store_kind\": ").Append(Str(info.storeKind)).Append(",\n");
sb.Append(" \"time_mode\": ").Append(Str(info.timeMode)).Append(",\n");
sb.Append(" \"time_scale\": ").Append(F(info.timeScale)).Append(",\n");
sb.Append(" \"capture_delta_time\": ").Append(F(info.captureDeltaTime)).Append(",\n");
sb.Append(" \"aisle_width_m\": ").Append(F(info.aisleWidthM)).Append(",\n");
sb.Append(" \"delivery_offset_m\": ").Append(F(info.deliveryOffsetM)).Append(",\n");
sb.Append(" \"delivery_offset_used_m\": ").Append(F(info.deliveryOffsetUsedM)).Append(",\n");
sb.Append(" \"crowd\": ").Append(Str(info.crowd)).Append(",\n");
sb.Append(" \"robots\": ").Append(info.robots).Append(",\n");
sb.Append(" \"seed\": ").Append(info.seed).Append(",\n");
sb.Append(" \"trips_requested\": ").Append(info.tripsRequested).Append(",\n");
sb.Append(" \"trips_done\": ").Append(info.tripsDone).Append(",\n");
sb.Append(" \"stopped_early\": ").Append(info.stoppedEarly ? "true" : "false").Append(",\n");
sb.Append(" \"sim_seconds\": ").Append(F(info.simSeconds)).Append(",\n");
sb.Append(" \"wall_seconds\": ").Append(F(info.wallSeconds)).Append(",\n");
sb.Append(" \"speedup\": ").Append(F(info.wallSeconds > 0.01f ? info.simSeconds / info.wallSeconds : 0f)).Append(",\n");
sb.Append(" \"frame_delta_s\": ");
AppendStats(sb, info.frameDeltas);
sb.Append(",\n");
sb.Append(" \"robot_step_m\": ");
AppendStats(sb, info.robotSteps);
sb.Append(",\n");
sb.Append(" \"people_step_m\": ");
AppendStats(sb, info.peopleSteps);
sb.Append(",\n");
// すり抜けの確かめ(検査3)。ロボットと人を分けて出す
int over = CountOver(info.robotSteps, 0.05f);
int overPeople = CountOver(info.peopleSteps, 0.05f);
sb.Append(" \"frames_over_005m\": ").Append(over).Append(",\n");
sb.Append(" \"frames_over_005m_ratio\": ")
.Append(F(info.robotSteps.Count > 0 ? (float)over / info.robotSteps.Count : 0f)).Append(",\n");
sb.Append(" \"people_frames_over_005m\": ").Append(overPeople).Append(",\n");
sb.Append(" \"people_frames_over_005m_ratio\": ")
.Append(F(info.peopleSteps.Count > 0 ? (float)overPeople / info.peopleSteps.Count : 0f)).Append(",\n");
// 椅子を引いた回数(B6)
sb.Append(" \"chair_pulls\": ").Append(info.chairPulls).Append(",\n");
sb.Append(" \"chair_pulls_by_reason\": {");
for (int i = 0; i < info.chairPullsByReason.Count; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(Str(info.chairPullsByReason[i].Key)).Append(": ")
.Append(info.chairPullsByReason[i].Value);
}
sb.Append("}\n");
sb.Append("}\n");
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
/// <summary>しきい値を超えたコマの数</summary>
static int CountOver(List<float> values, float limit)
{
if (values == null) return 0;
int n = 0;
for (int i = 0; i < values.Count; i++) if (values[i] > limit) n++;
return n;
}
static void AppendStats(StringBuilder sb, List<float> values)
{
if (values == null || values.Count == 0) { sb.Append("null"); return; }
float sum = 0f;
for (int i = 0; i < values.Count; i++) sum += values[i];
sb.Append("{\"count\": ").Append(values.Count);
sb.Append(", \"mean\": ").Append(F(sum / values.Count));
sb.Append(", \"min\": ").Append(F(Percentile(values, 0f)));
sb.Append(", \"p50\": ").Append(F(Percentile(values, 0.5f)));
sb.Append(", \"p90\": ").Append(F(Percentile(values, 0.9f)));
sb.Append(", \"p99\": ").Append(F(Percentile(values, 0.99f)));
sb.Append(", \"max\": ").Append(F(Percentile(values, 1f)));
sb.Append("}");
}
// ---------------- conditions.json ----------------
/// <summary>
/// 18条件の中身と、設定ファイル(.asset)の値すべてを書く。
/// 値は実行したときのものを JsonUtility でそのまま出す。
/// </summary>
public static void WriteConditionsJson(string path, string runName,
StoreLayout layout, RobotSpec robotSpec,
PeopleSpec peopleSpec, ExperimentPlan plan,
List<ExperimentPlan.Condition> conditions)
{
StringBuilder sb = new StringBuilder();
sb.Append("{\n");
sb.Append(" \"run_name\": ").Append(Str(runName)).Append(",\n");
sb.Append(" \"written_at\": ").Append(Str(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))).Append(",\n");
sb.Append(" \"unity_version\": ").Append(Str(Application.unityVersion)).Append(",\n");
sb.Append(" \"trips_csv_columns\": ").Append(Str(Header)).Append(",\n");
sb.Append(" \"settings\": {\n");
sb.Append(" \"store_layout\": ").Append(Dump(layout)).Append(",\n");
sb.Append(" \"robot_spec\": ").Append(Dump(robotSpec)).Append(",\n");
sb.Append(" \"people_spec\": ").Append(Dump(peopleSpec)).Append(",\n");
sb.Append(" \"experiment_plan\": ").Append(Dump(plan)).Append("\n");
sb.Append(" },\n");
// 組み立てた形から出る値(conditions.json だけ見れば店の大きさが分かるように)
sb.Append(" \"derived\": {\n");
if (layout != null)
{
StorePlan sp = layout.NewPlan();
sb.Append(" \"store_kind\": ").Append(Str(layout.StoreKindName)).Append(",\n");
sb.Append(" \"aisle_width_m\": ").Append(F(layout.AisleWidth)).Append(",\n");
if (sp != null)
{
sb.Append(" \"floor_min_x\": ").Append(F(sp.floor.xMin)).Append(",\n");
sb.Append(" \"floor_max_x\": ").Append(F(sp.floor.xMax)).Append(",\n");
sb.Append(" \"floor_min_z\": ").Append(F(sp.floor.zMin)).Append(",\n");
sb.Append(" \"floor_max_z\": ").Append(F(sp.floor.zMax)).Append(",\n");
sb.Append(" \"outer_size_x_m\": ").Append(F(sp.floor.SizeX)).Append(",\n");
sb.Append(" \"outer_size_z_m\": ").Append(F(sp.floor.SizeZ)).Append(",\n");
sb.Append(" \"table_count\": ").Append(sp.seats.Count).Append(",\n");
sb.Append(" \"aisle_count\": ").Append(sp.aisles.Count).Append(",\n");
sb.Append(" \"narrow_aisle_count\": ").Append(sp.NarrowAisleCount).Append("\n");
}
else
{
sb.Append(" \"plan\": null\n");
}
}
sb.Append(" },\n");
sb.Append(" \"conditions\": [\n");
if (conditions != null)
{
for (int i = 0; i < conditions.Count; i++)
{
ExperimentPlan.Condition c = conditions[i];
sb.Append(" {");
sb.Append("\"condition_id\": ").Append(Str(c.id)).Append(", ");
sb.Append("\"aisle_width_m\": ").Append(F(c.aisleWidthM)).Append(", ");
sb.Append("\"aisle_width_index\": ").Append(c.aisleWidthIndex).Append(", ");
sb.Append("\"crowd\": ").Append(Str(ExperimentPlan.CrowdName(c.crowd))).Append(", ");
sb.Append("\"walkers\": ").Append(peopleSpec != null ? peopleSpec.WalkerCount(c.crowd) : 0).Append(", ");
sb.Append("\"robots\": ").Append(c.robots).Append(", ");
sb.Append("\"trips\": ").Append(plan != null ? plan.tripsPerCondition : 0).Append(", ");
sb.Append("\"seeds\": ").Append(IntArray(plan != null ? plan.seeds : null)).Append(", ");
sb.Append("\"aisle_yielding\": ").Append(robotSpec != null && robotSpec.aisleYielding ? "true" : "false");
sb.Append('}');
if (i < conditions.Count - 1) sb.Append(',');
sb.Append('\n');
}
}
sb.Append(" ]\n");
sb.Append("}\n");
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
static string Dump(ScriptableObject asset)
{
if (asset == null) return "null";
return JsonUtility.ToJson(asset, false);
}
static string IntArray(int[] values)
{
if (values == null || values.Length == 0) return "[]";
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < values.Length; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(values[i]);
}
return sb.Append(']').ToString();
}
static string Str(string s)
{
if (s == null) return "null";
StringBuilder sb = new StringBuilder("\"");
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (c == '"' || c == '\\') sb.Append('\\').Append(c);
else if (c == '\n') sb.Append("\\n");
else if (c == '\r') sb.Append("\\r");
else if (c == '\t') sb.Append("\\t");
else sb.Append(c);
}
return sb.Append('"').ToString();
}
// ---------------- summary.json ----------------
/// <summary>条件ごとにまとめる</summary>
/// <summary>試算(haizen_sim.py)と同じ意味の数字。名前も同じにする</summary>
public class SimLike
{
public List<float> dishWaitMin = new List<float>();
public List<float> seatWaitMin = new List<float>();
public List<float> turnMin = new List<float>();
public float coldMin = 3f;
public int robotDishes, staffDishes, requeuedDishes, groups;
/// <summary>あきらめた・移した・卓を空けた回数(B1)</summary>
public int gaveUp, warped, tableFreed;
/// <summary>人どうしの詰まりのうち、近くでロボットが止まっていたもの(B9)</summary>
public int robotBlockedJams;
/// <summary>人がロボットに近づきすぎた回数・待った回数と秒数・回り道した回数</summary>
public int robotContacts, robotYieldWaits, robotYieldDetours;
public float robotYieldWaitSeconds;
/// <summary>レジの前にいた人数(いちばん多いとき・平均)</summary>
public int registerCrowdMax;
public float registerCrowdMean;
/// <summary>会計の回数と、そのうちスタッフが店員側にいなかった回数(0 になるはず)</summary>
public int registerPayCount, registerPayNotAtBackCount;
/// <summary>立つ点から離れて会計した回数(0 になるはず)と、その距離</summary>
public int registerPayOffSpotCount;
public float registerPayMeanOffsetM, registerPayMaxOffsetM;
/// <summary>会計でないスタッフが店員側にいた回数と秒数(どちらも 0 になるはず)</summary>
public int registerBackIntruderCount;
public float registerBackIntruderSeconds;
/// <summary>スタッフの時間の使い方(仕事の名前 → 秒)</summary>
public readonly List<KeyValuePair<string, float>> staffTime =
new List<KeyValuePair<string, float>>();
/// <summary>仕事が板に出てから取られるまでの秒数の平均(仕事の名前 → 秒)</summary>
public readonly List<KeyValuePair<string, float>> jobWaitMean =
new List<KeyValuePair<string, float>>();
/// <summary>届いた料理の、届くまでの内訳(ロボット・スタッフ別に分ける)</summary>
public readonly List<ServiceOps.DishParts> dishParts = new List<ServiceOps.DishParts>();
/// <summary>椅子を引いた回数(B6)</summary>
public int chairPulls;
/// <summary>ドリンクバー:並んだ回数・あきらめた回数・並んで待った分(B4)</summary>
public int drinkBarQueued, drinkBarGaveUp;
public readonly List<float> drinkBarWaitMin = new List<float>();
public int jamCount; public float jamTotalS;
public float walkersMean;
public float measuredMinutes;
}
SimLike _sim;
public void SetSimLike(SimLike s) { _sim = s; }
public void WriteSummaryJson(string path, string runName)
{
// 条件ごとに行を分ける(出てきた順を保つ)
List<string> order = new List<string>();
Dictionary<string, List<Row>> byCondition = new Dictionary<string, List<Row>>();
for (int i = 0; i < _rows.Count; i++)
{
string id = _rows[i].conditionId;
if (!byCondition.ContainsKey(id)) { byCondition[id] = new List<Row>(); order.Add(id); }
byCondition[id].Add(_rows[i]);
}
StringBuilder sb = new StringBuilder();
sb.Append("{\n");
sb.Append(" \"run_name\": ").Append(Str(runName)).Append(",\n");
sb.Append(" \"written_at\": ").Append(Str(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"))).Append(",\n");
sb.Append(" \"note\": ").Append(Str(
"robot_speed_mps = distance_m / (total_s - stopped_s)。" +
"stopped_s には、動けなくなった60秒+15秒と、通路の入口で待った秒数も入る。" +
"pause_s は stop_durations_s(自分で動き出した止まりだけ)の分布。")).Append(",\n");
if (_sim != null)
{
sb.Append(" \"sim_like\": {");
sb.Append("\"dish_wait_mean\": ").Append(F(Mean(_sim.dishWaitMin))).Append(", ");
sb.Append("\"dish_wait_p90\": ").Append(F(Percentile(_sim.dishWaitMin, 0.9f))).Append(", ");
sb.Append("\"cold_share\": ").Append(F(Share(_sim.dishWaitMin, _sim.coldMin))).Append(", ");
sb.Append("\"seat_wait_mean\": ").Append(F(Mean(_sim.seatWaitMin))).Append(", ");
sb.Append("\"turn_mean\": ").Append(F(Mean(_sim.turnMin))).Append(", ");
sb.Append("\"robot_share\": ").Append(F(_sim.robotDishes + _sim.staffDishes > 0
? (float)_sim.robotDishes / (_sim.robotDishes + _sim.staffDishes) : 0f)).Append(", ");
sb.Append("\"robot_dishes\": ").Append(_sim.robotDishes).Append(", ");
sb.Append("\"staff_dishes\": ").Append(_sim.staffDishes).Append(", ");
sb.Append("\"requeued_dishes\": ").Append(_sim.requeuedDishes).Append(", ");
sb.Append("\"groups\": ").Append(_sim.groups).Append(", ");
sb.Append("\"people_jams\": ").Append(_sim.jamCount).Append(", ");
sb.Append("\"robot_blocked_jams\": ").Append(_sim.robotBlockedJams).Append(", ");
sb.Append("\"robot_blocked_jam_share\": ").Append(F(_sim.jamCount > 0
? (float)_sim.robotBlockedJams / _sim.jamCount : 0f)).Append(", ");
sb.Append("\"chair_pulls\": ").Append(_sim.chairPulls).Append(", ");
sb.Append("\"robot_contacts\": ").Append(_sim.robotContacts).Append(", ");
sb.Append("\"robot_yield_waits\": ").Append(_sim.robotYieldWaits).Append(", ");
sb.Append("\"robot_yield_wait_s\": ").Append(F(_sim.robotYieldWaitSeconds)).Append(", ");
sb.Append("\"robot_yield_detours\": ").Append(_sim.robotYieldDetours).Append(", ");
sb.Append("\"register_crowd_max\": ").Append(_sim.registerCrowdMax).Append(", ");
sb.Append("\"register_crowd_mean\": ").Append(F(_sim.registerCrowdMean)).Append(", ");
sb.Append("\"register_pay_count\": ").Append(_sim.registerPayCount).Append(", ");
sb.Append("\"register_pay_not_at_back\": ").Append(_sim.registerPayNotAtBackCount).Append(", ");
sb.Append("\"register_pay_off_spot\": ").Append(_sim.registerPayOffSpotCount).Append(", ");
sb.Append("\"register_pay_offset_mean_m\": ").Append(F(_sim.registerPayMeanOffsetM)).Append(", ");
sb.Append("\"register_pay_offset_max_m\": ").Append(F(_sim.registerPayMaxOffsetM)).Append(", ");
sb.Append("\"register_back_intruders\": ").Append(_sim.registerBackIntruderCount).Append(", ");
sb.Append("\"register_back_intruder_s\": ").Append(F(_sim.registerBackIntruderSeconds)).Append(", ");
sb.Append("\"drink_bar_queued\": ").Append(_sim.drinkBarQueued).Append(", ");
sb.Append("\"drink_bar_gave_up\": ").Append(_sim.drinkBarGaveUp).Append(", ");
sb.Append("\"drink_bar_wait_mean\": ").Append(F(Mean(_sim.drinkBarWaitMin))).Append(", ");
sb.Append("\"drink_bar_wait_p90\": ").Append(F(Percentile(_sim.drinkBarWaitMin, 0.9f))).Append(", ");
sb.Append("\"gave_up\": ").Append(_sim.gaveUp).Append(", ");
sb.Append("\"warped\": ").Append(_sim.warped).Append(", ");
sb.Append("\"table_freed\": ").Append(_sim.tableFreed).Append(", ");
sb.Append("\"people_jam_total_s\": ").Append(F(_sim.jamTotalS)).Append(", ");
sb.Append("\"walkers_mean\": ").Append(F(_sim.walkersMean)).Append(", ");
sb.Append("\"measured_minutes\": ").Append(F(_sim.measuredMinutes));
sb.Append("},\n");
// スタッフの時間の使い方(割合も出す)
float staffTotal = 0f;
for (int i = 0; i < _sim.staffTime.Count; i++) staffTotal += _sim.staffTime[i].Value;
sb.Append(" \"staff_time_s\": {");
for (int i = 0; i < _sim.staffTime.Count; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(Str(_sim.staffTime[i].Key)).Append(": ").Append(F(_sim.staffTime[i].Value));
}
sb.Append("},\n");
// 料理ができてから届くまでの内訳(受け渡し口で待った/運んでいた/戻されて失った)
sb.Append(" \"dish_wait_parts\": {");
AppendDishParts(sb, "robot", true);
sb.Append(", ");
AppendDishParts(sb, "staff", false);
sb.Append(", ");
AppendDishParts(sb, "all", null);
sb.Append("},\n");
sb.Append(" \"job_wait_mean_s\": {");
for (int i = 0; i < _sim.jobWaitMean.Count; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(Str(_sim.jobWaitMean[i].Key)).Append(": ").Append(F(_sim.jobWaitMean[i].Value));
}
sb.Append("},\n");
sb.Append(" \"staff_time_share\": {");
for (int i = 0; i < _sim.staffTime.Count; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(Str(_sim.staffTime[i].Key)).Append(": ")
.Append(F(staffTotal > 0.01f ? _sim.staffTime[i].Value / staffTotal : 0f));
}
sb.Append("},\n");
}
// 止まり全体のまとめ(B7 の前後を並べるため)
sb.Append(" \"stops\": {");
sb.Append("\"count\": ").Append(_stops.Count).Append(", ");
sb.Append("\"stuck_count\": ").Append(StuckStopCount()).Append(", ");
sb.Append("\"partial_path_count\": ").Append(PartialPathStopCount());
sb.Append("},\n");
sb.Append(" \"stop_causes\": [\n");
AppendCauseSummary(sb);
sb.Append(" ],\n");
sb.Append(" \"conditions\": [\n");
for (int i = 0; i < order.Count; i++)
{
List<Row> rows = byCondition[order[i]];
AppendConditionSummary(sb, order[i], rows);
if (i < order.Count - 1) sb.Append(',');
sb.Append('\n');
}
sb.Append(" ]\n");
sb.Append("}\n");
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
/// <summary>
/// 原因ごとの回数と秒数の合計。
/// stuck_count は「動けなくなった」で終わった止まり(止まりの上限に達した回数)。
/// partial_path_count は「途中までの経路」に縛られて引き直して出た止まり(B7)。
/// この2つを、停止点を変えた2回で並べれば「椅子が原因で上限に達した回数」の差が出る。
/// </summary>
void AppendCauseSummary(StringBuilder sb)
{
string stuckLabel = ResolveLabel(ServingRobotController.StopResolve.Stuck);
List<string> order = new List<string>();
Dictionary<string, int> count = new Dictionary<string, int>();
Dictionary<string, float> secs = new Dictionary<string, float>();
Dictionary<string, float> longest = new Dictionary<string, float>();
Dictionary<string, int> blocked = new Dictionary<string, int>();
Dictionary<string, int> stuck = new Dictionary<string, int>();
Dictionary<string, int> partial = new Dictionary<string, int>();
Dictionary<string, float> noPath = new Dictionary<string, float>();
for (int i = 0; i < _stops.Count; i++)
{
string c = _stops[i].cause;
if (!count.ContainsKey(c))
{
order.Add(c); count[c] = 0; secs[c] = 0f; longest[c] = 0f; blocked[c] = 0;
stuck[c] = 0; partial[c] = 0; noPath[c] = 0f;
}
count[c]++;
secs[c] += _stops[i].durationS;
if (_stops[i].durationS > longest[c]) longest[c] = _stops[i].durationS;
if (_stops[i].pathBlocked) blocked[c]++;
if (_stops[i].resolved == stuckLabel) stuck[c]++;
if (HasPartialPathMark(_stops[i].causeDetail)) partial[c]++;
noPath[c] += _stops[i].noPathS;
}
for (int i = 0; i < order.Count; i++)
{
string c = order[i];
sb.Append(" {\"cause\": ").Append(Str(c))
.Append(", \"count\": ").Append(count[c])
.Append(", \"total_s\": ").Append(F(secs[c]))
.Append(", \"mean_s\": ").Append(F(count[c] > 0 ? secs[c] / count[c] : 0f))
.Append(", \"longest_s\": ").Append(F(longest[c]))
.Append(", \"path_blocked_count\": ").Append(blocked[c])
.Append(", \"stuck_count\": ").Append(stuck[c])
.Append(", \"partial_path_count\": ").Append(partial[c])
.Append(", \"no_path_total_s\": ").Append(F(noPath[c]))
.Append('}');
if (i < order.Count - 1) sb.Append(',');
sb.Append('\n');
}
}
/// <summary>
/// 届くまでの内訳を1つぶん書く。byRobot が null なら全部。
/// 秒は「分」に直して出す(dish_wait_mean と同じ単位にそろえる)。
/// </summary>
void AppendDishParts(StringBuilder sb, string name, bool? byRobot)
{
int n = 0, requeued = 0;
float pass = 0f, carry = 0f, requeue = 0f;
if (_sim != null)
{
for (int i = 0; i < _sim.dishParts.Count; i++)
{
ServiceOps.DishParts p = _sim.dishParts[i];
if (byRobot.HasValue && p.byRobot != byRobot.Value) continue;
n++;
pass += p.passWaitS;
carry += p.carryS;
requeue += p.requeueS;
requeued += p.requeued;
}
}
float d = n > 0 ? n : 1f;
sb.Append(Str(name)).Append(": {");
sb.Append("\"dishes\": ").Append(n).Append(", ");
sb.Append("\"pass_wait_mean_min\": ").Append(F(pass / d / 60f)).Append(", ");
sb.Append("\"carry_mean_min\": ").Append(F(carry / d / 60f)).Append(", ");
sb.Append("\"requeue_lost_mean_min\": ").Append(F(requeue / d / 60f)).Append(", ");
sb.Append("\"total_mean_min\": ").Append(F((pass + carry + requeue) / d / 60f)).Append(", ");
sb.Append("\"requeued_per_dish\": ").Append(F(requeued / d)).Append(", ");
sb.Append("\"requeued_total\": ").Append(requeued);
sb.Append("}");
}
/// <summary>この止まりが「途中までの経路」に縛られたものか(B7)</summary>
static bool HasPartialPathMark(string causeDetail)
{
return !string.IsNullOrEmpty(causeDetail) &&
causeDetail.Contains(ServingRobotController.PartialPathMark);
}
/// <summary>「動けなくなった」で終わった止まりの数(止まりの上限に達した回数)</summary>
int StuckStopCount()
{
string label = ResolveLabel(ServingRobotController.StopResolve.Stuck);
int n = 0;
for (int i = 0; i < _stops.Count; i++) if (_stops[i].resolved == label) n++;
return n;
}
/// <summary>「途中までの経路」から引き直して出た止まりの数(B7)</summary>
int PartialPathStopCount()
{
int n = 0;
for (int i = 0; i < _stops.Count; i++) if (HasPartialPathMark(_stops[i].causeDetail)) n++;
return n;
}
static void AppendConditionSummary(StringBuilder sb, string id, List<Row> rows)
{
List<float> speeds = new List<float>();
List<float> stopped = new List<float>();
List<float> pauses = new List<float>();
List<float> waits = new List<float>();
int stuckCount = 0, timeoutCount = 0, pausedCount = 0, metRobotCount = 0;
float aisleWidth = 0f; int robots = 0; string crowd = "";
for (int i = 0; i < rows.Count; i++)
{
Row r = rows[i];
aisleWidth = r.aisleWidthM; robots = r.robots; crowd = r.crowd;
float moving = r.totalS - r.stoppedS;
if (moving > 0.01f) speeds.Add(r.distanceM / moving);
stopped.Add(r.stoppedS);
waits.Add(r.waitAisleS);
if (r.stopDurationsS != null)
for (int k = 0; k < r.stopDurationsS.Length; k++) pauses.Add(r.stopDurationsS[k]);
if (r.stuck) stuckCount++;
if (r.timeout) timeoutCount++;
if (r.nStops >= 1) pausedCount++;
if (r.metRobot) metRobotCount++;
}
int n = rows.Count;
sb.Append(" {");
sb.Append("\"condition_id\": ").Append(Str(id)).Append(", ");
sb.Append("\"aisle_width_m\": ").Append(F(aisleWidth)).Append(", ");
sb.Append("\"crowd\": ").Append(Str(crowd)).Append(", ");
sb.Append("\"robots\": ").Append(robots).Append(", ");
sb.Append("\"trips\": ").Append(n).Append(", ");
sb.Append("\"robot_speed_mps_median\": ").Append(F(Percentile(speeds, 0.5f))).Append(", ");
sb.Append("\"stopped_s_median\": ").Append(F(Percentile(stopped, 0.5f))).Append(", ");
sb.Append("\"stopped_s_p90\": ").Append(F(Percentile(stopped, 0.9f))).Append(", ");
sb.Append("\"pause_s_median\": ").Append(F(Percentile(pauses, 0.5f))).Append(", ");
sb.Append("\"pause_s_p90\": ").Append(F(Percentile(pauses, 0.9f))).Append(", ");
sb.Append("\"pause_count\": ").Append(pauses.Count).Append(", ");
sb.Append("\"wait_aisle_s_median\": ").Append(F(Percentile(waits, 0.5f))).Append(", ");
sb.Append("\"stuck_rate\": ").Append(F(n > 0 ? stuckCount / (float)n : 0f)).Append(", ");
sb.Append("\"stuck_count\": ").Append(stuckCount).Append(", ");
sb.Append("\"timeout_rate\": ").Append(F(n > 0 ? timeoutCount / (float)n : 0f)).Append(", ");
sb.Append("\"timeout_count\": ").Append(timeoutCount).Append(", ");
sb.Append("\"pause_rate\": ").Append(F(n > 0 ? pausedCount / (float)n : 0f)).Append(", ");
sb.Append("\"met_robot_rate\": ").Append(F(n > 0 ? metRobotCount / (float)n : 0f));
sb.Append('}');
}
static float Mean(List<float> v)
{
if (v == null || v.Count == 0) return -1f;
float s = 0f;
for (int i = 0; i < v.Count; i++) s += v[i];
return s / v.Count;
}
static float Share(List<float> v, float over)
{
if (v == null || v.Count == 0) return -1f;
int n = 0;
for (int i = 0; i < v.Count; i++) if (v[i] > over) n++;
return (float)n / v.Count;
}
/// <summary>小さい順に並べて p の位置の値を返す(0〜1)。空なら -1</summary>
public static float Percentile(List<float> values, float p)
{
if (values == null || values.Count == 0) return -1f;
List<float> v = new List<float>(values);
v.Sort();
if (v.Count == 1) return v[0];
float pos = Mathf.Clamp01(p) * (v.Count - 1);
int lo = Mathf.FloorToInt(pos);
int hi = Mathf.Min(lo + 1, v.Count - 1);
float t = pos - lo;
return Mathf.Lerp(v[lo], v[hi], t);
}
}
}
Editor/ServiceSimCameras.cs
using UnityEngine;
namespace ServiceSim.EditorTools
{
/// <summary>
/// 報告用のカメラの位置を、組み立てた店の広さから決める。
/// 新しい数字は持たず、床の広がりから計算する。
/// </summary>
public static class ServiceSimCameras
{
const float Margin = 0.8f; // 店のまわりに取る余白(m)
const float TopHeight = 60f; // 真上のカメラの高さ(m)
/// <summary>真上から見る(直交投影)。+Z が画面の上</summary>
public static void FrameTop(Camera cam, PlanRect floor, float aspect)
{
if (cam == null) return;
PlanRect r = floor.Expand(Margin);
cam.orthographic = true;
cam.orthographicSize = Mathf.Max(r.SizeZ * 0.5f, (r.SizeX * 0.5f) / Mathf.Max(0.01f, aspect));
cam.nearClipPlane = 0.1f;
cam.farClipPlane = TopHeight + 20f;
cam.transform.position = new Vector3(r.CenterX, TopHeight, r.CenterZ);
cam.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
}
/// <summary>斜め上から見る(透視投影)。店全体が入るよう、南東から引いて見下ろす</summary>
public static void FrameOblique(Camera cam, PlanRect floor)
{
if (cam == null) return;
PlanRect r = floor.Expand(Margin);
Vector3 center = new Vector3(r.CenterX, 0f, r.CenterZ);
float span = Mathf.Max(r.SizeX, r.SizeZ);
Vector3 pos = center + new Vector3(span * 0.55f, span * 0.85f, -span * 0.95f);
cam.orthographic = false;
cam.fieldOfView = 45f;
cam.nearClipPlane = 0.1f;
cam.farClipPlane = span * 4f + 50f;
cam.transform.position = pos;
cam.transform.rotation = Quaternion.LookRotation((center - pos).normalized, Vector3.up);
}
}
}
Editor/ServiceSimLabels.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace ServiceSim.EditorTools
{
/// <summary>
/// 人とロボットが「いま何をしているか」を、頭の上に1行で出す(Scene ビューだけ)。
///
/// 見るだけのもの。読むのは、人とロボットのスクリプトがすでに持っている
/// 「今の仕事」の値だけで、表示のための値は持たない。
/// 当たり判定も処理の順番も変えない(Editor 側で描くだけ)。
///
/// 動画に映す表示(Game ビュー)は、段階4で設計書 8.6 と一緒に作る。
/// </summary>
[InitializeOnLoad]
public static class ServiceSimLabels
{
const string ShowKey = "ServiceSim.ShowLabels";
const string SeatedKey = "ServiceSim.ShowSeatedLabels";
const string WhoKey = "ServiceSim.LabelWho";
const string MenuShow = "ServiceSim/Show Labels";
const string MenuSeated = "ServiceSim/Show Labels (椅子を引いている客も)";
const string MenuWhoRobot = "ServiceSim/Show Labels/ロボットだけ";
const string MenuWhoStaff = "ServiceSim/Show Labels/ロボットとスタッフ";
const string MenuWhoAll = "ServiceSim/Show Labels/全員";
/// <summary>出す相手(B5-3)</summary>
enum Who { RobotOnly = 0, RobotAndStaff = 1, All = 2 }
// ---- 見た目(B5)----
/// <summary>字の大きさ(点)。Scene ビューの既定より大きくする</summary>
const int FontSize = 13;
/// <summary>帯どうしが重ならないように、上下にずらす幅(画面の点)</summary>
const float RowHeightPx = 17f;
/// <summary>この距離(画面の点)より近い帯は、重なっているとみなして上へずらす</summary>
const float OverlapPx = 90f;
static GUIStyle _style;
static StoreBuilder _store;
static readonly List<StoreBuilder.TableInfo> _tables = new List<StoreBuilder.TableInfo>();
/// <summary>この回に描いた帯の画面上の場所(重なりを避けるのに使う)</summary>
static readonly List<Rect> _drawn = new List<Rect>();
// 誰の字幕かを分ける、帯の左端の色(B5-1)
static readonly Color TagRobot = new Color(0.95f, 0.95f, 0.97f);
static readonly Color TagRobotStopped = new Color(1.00f, 0.85f, 0.25f);
static readonly Color TagRobotStuck = new Color(1.00f, 0.30f, 0.25f);
static readonly Color TagStaff = new Color(0.20f, 0.55f, 1.00f);
static readonly Color TagGuest = new Color(1.00f, 0.82f, 0.15f);
static readonly Color TagWaiting = new Color(0.65f, 0.40f, 0.95f);
static readonly Color TagChair = new Color(1.00f, 0.55f, 0.25f);
static bool Show
{
get { return EditorPrefs.GetBool(ShowKey, false); }
set { EditorPrefs.SetBool(ShowKey, value); }
}
static bool ShowSeated
{
get { return EditorPrefs.GetBool(SeatedKey, true); }
set { EditorPrefs.SetBool(SeatedKey, value); }
}
/// <summary>出す相手。初期値はロボットとスタッフ(B5-3)</summary>
static Who ShowWho
{
get { return (Who)EditorPrefs.GetInt(WhoKey, (int)Who.RobotAndStaff); }
set { EditorPrefs.SetInt(WhoKey, (int)value); }
}
static ServiceSimLabels()
{
SceneView.duringSceneGui -= OnSceneGui;
SceneView.duringSceneGui += OnSceneGui;
}
/// <summary>
/// 計測の実行中は字幕を出さない(B11)。
/// 字幕は Scene ビューで毎コマ描くので、35卓ぶんあると倍速に効く。
/// メニューの設定(EditorPrefs)は変えない。回し終われば自動で元に戻る。
/// </summary>
static bool MeasuringNow
{
get
{
if (!EditorApplication.isPlaying) return false;
ExperimentRunner r = UnityEngine.Object.FindObjectOfType<ExperimentRunner>();
if (r == null || !r.Running) return false;
// 見る用の回(Run One > 見る用)は消さない。
// ExperimentPlan で「回している間も出す」にしてあるときも消さない
if (r.viewMode) return false;
return r.plan == null || !r.plan.showLabelsWhileRunning;
}
}
[MenuItem(MenuShow, false, 100)]
static void Toggle()
{
Show = !Show;
SceneView.RepaintAll();
Debug.Log("[ServiceSim] 頭の上の字幕: " + (Show ? "出す" : "消す") + "(Scene ビューだけ)");
}
[MenuItem(MenuShow, true)]
static bool ToggleValidate() { Menu.SetChecked(MenuShow, Show); return true; }
[MenuItem("ServiceSim/Show Labels (いまの出し方を見る)", false, 102)]
static void Report()
{
Debug.Log("[ServiceSim] 頭の上の字幕: " + (Show ? "出す" : "消す") +
" / 椅子を引いている客も: " + (ShowSeated ? "出す" : "消す") +
"\n 計測の実行中は自動で切る(いまは " + (MeasuringNow ? "計測中なので出していません" : "計測していません") + ")");
}
[MenuItem(MenuSeated, false, 101)]
static void ToggleSeated() { ShowSeated = !ShowSeated; SceneView.RepaintAll(); }
[MenuItem(MenuSeated, true)]
static bool ToggleSeatedValidate() { Menu.SetChecked(MenuSeated, ShowSeated); return Show; }
[MenuItem(MenuWhoRobot, false, 110)]
static void WhoRobot() { ShowWho = Who.RobotOnly; SceneView.RepaintAll(); }
[MenuItem(MenuWhoRobot, true)]
static bool WhoRobotValidate()
{
Menu.SetChecked(MenuWhoRobot, ShowWho == Who.RobotOnly);
return Show;
}
[MenuItem(MenuWhoStaff, false, 111)]
static void WhoStaff() { ShowWho = Who.RobotAndStaff; SceneView.RepaintAll(); }
[MenuItem(MenuWhoStaff, true)]
static bool WhoStaffValidate()
{
Menu.SetChecked(MenuWhoStaff, ShowWho == Who.RobotAndStaff);
return Show;
}
[MenuItem(MenuWhoAll, false, 112)]
static void WhoAll() { ShowWho = Who.All; SceneView.RepaintAll(); }
[MenuItem(MenuWhoAll, true)]
static bool WhoAllValidate()
{
Menu.SetChecked(MenuWhoAll, ShowWho == Who.All);
return Show;
}
// ---------------- 描く ----------------
static void OnSceneGui(SceneView view)
{
if (!Show) return;
if (MeasuringNow) return; // 計測中は出さない(B11)
EnsureStyle();
RefreshStore();
_drawn.Clear();
Handles.BeginGUI();
DrawRobots();
if (ShowWho != Who.RobotOnly) DrawStaff();
if (ShowWho == Who.All) DrawGuests();
if (ShowSeated) DrawSeated(); // 椅子を引いている卓は、相手の設定に関わらず出す
Handles.EndGUI();
}
static void EnsureStyle()
{
if (_style != null) return;
// 日本語は、エディタの既定のフォントがそのまま出る(font は差し替えない)
_style = new GUIStyle(EditorStyles.label);
_style.alignment = TextAnchor.MiddleLeft;
_style.fontStyle = FontStyle.Bold;
_style.fontSize = FontSize;
_style.normal.background = null;
_style.padding = new RectOffset(6, 6, 2, 2);
_style.normal.textColor = Color.white;
}
static void RefreshStore()
{
if (_store == null) _store = Object.FindObjectOfType<StoreBuilder>();
_tables.Clear();
if (_store == null) return;
IList<StoreBuilder.TableInfo> t = _store.Tables;
for (int i = 0; i < t.Count; i++) _tables.Add(t[i]);
}
/// <summary>
/// 頭の上に1行の字幕を出す(B5)。
/// 濃い半透明の帯を敷いて文字は白にし、誰の字幕かは帯の左端の色で分ける。
/// 近くの帯どうしは上へずらして重ならないようにする。
/// </summary>
static void Label(Transform tr, float heightM, string text, Color tag)
{
if (tr == null || string.IsNullOrEmpty(text)) return;
Camera cam = Camera.current;
if (cam == null) return;
Vector3 world = tr.position + Vector3.up * heightM;
Vector3 vp = cam.WorldToViewportPoint(world);
if (vp.z <= 0f || vp.x < -0.1f || vp.x > 1.1f || vp.y < -0.1f || vp.y > 1.1f) return;
Vector2 size = _style.CalcSize(new GUIContent(text));
float w = size.x + 8f; // 左端の色の帯のぶん
float h = Mathf.Max(size.y, RowHeightPx);
// 画面の座標(GUI は左上が原点)
Vector3 sp = HandleUtility.WorldToGUIPointWithDepth(world);
if (sp.z < 0f) return;
Rect rect = new Rect(sp.x - w * 0.5f, sp.y - h, w, h);
rect = AvoidOverlap(rect);
_drawn.Add(rect);
// 帯(濃い半透明)
EditorGUI.DrawRect(rect, new Color(0.05f, 0.06f, 0.09f, 0.82f));
// 左端の色(誰の字幕か)
EditorGUI.DrawRect(new Rect(rect.x, rect.y, 4f, rect.height), tag);
// 文字(白)
GUI.Label(new Rect(rect.x + 4f, rect.y, rect.width - 4f, rect.height), text, _style);
}
/// <summary>
/// すでに描いた帯と重なるなら、重ならなくなるまで上へずらす。
/// 近い帯どうしだけを見る(画面の離れたところは見ない)。
/// </summary>
static Rect AvoidOverlap(Rect rect)
{
for (int guard = 0; guard < 12; guard++)
{
bool moved = false;
for (int i = 0; i < _drawn.Count; i++)
{
Rect o = _drawn[i];
if (Mathf.Abs(o.center.x - rect.center.x) > OverlapPx) continue;
if (!rect.Overlaps(o)) continue;
rect.y = o.y - rect.height - 2f;
moved = true;
}
if (!moved) break;
}
return rect;
}
// ---------------- ロボット ----------------
static void DrawRobots()
{
IReadOnlyList<ServingRobotController> robots = ServingRobotController.All;
for (int i = 0; i < robots.Count; i++)
{
ServingRobotController r = robots[i];
if (r == null || r.spec == null) continue;
Color tag = TagRobot;
string text = RobotText(r, ref tag);
Label(r.transform, r.spec.height + 0.35f, "[" + r.robotId + "] " + text, tag);
}
}
/// <summary>
/// ロボットの字幕(B5-5 で短くした言い方)。
/// 止まっている台は帯を黄、動けない台は帯を赤にする(B5-1)。
/// </summary>
static string RobotText(ServingRobotController r, ref Color tag)
{
switch (r.CurrentPhase)
{
case ServingRobotController.Phase.Idle: return "待機";
case ServingRobotController.Phase.Done: return "走りおわり";
case ServingRobotController.Phase.Dwell:
return "卓" + r.TableId.ToString("00");
case ServingRobotController.Phase.StuckWait:
tag = TagRobotStuck;
return "動けない " + Mathf.CeilToInt(r.StuckWaitRemainS) + "秒";
case ServingRobotController.Phase.ToTable:
case ServingRobotController.Phase.ToPass:
if (r.YieldHolding)
{
tag = TagRobotStopped;
return "入口待ち";
}
if (r.SpeedMps <= r.spec.stoppedSpeedEpsilon)
{
tag = TagRobotStopped;
return "停止:" + StopCauseText(r);
}
// 走っているときは行き先の卓だけ(場所は記録に出るので書かない)
return r.CurrentPhase == ServingRobotController.Phase.ToTable
? "卓" + r.TableId.ToString("00") + TableListText(r)
: "戻り";
}
return "";
}
/// <summary>1走りで2卓以上回るときは、卓の数だけ添える(短く)</summary>
static string TableListText(ServingRobotController r)
{
if (r.TableIds.Count <= 1) return "";
return "(" + r.TableIds.Count + "卓)";
}
/// <summary>
/// 止まった原因だけを短く。センサーがすでに持っている値から作る
/// (「客」「スタッフ」「椅子」「ロボット」「道がない」)。
/// </summary>
static string StopCauseText(ServingRobotController r)
{
if (r.sensor == null) return "不明";
FrontSensor.Reading s = r.sensor.Last;
if (!s.blocked) return r.PathComplete ? "不明" : "道がない";
if (s.nearestIsRobot) return "ロボット";
if (s.nearestIsChair) return "椅子";
if (s.nearestIsPerson)
return s.nearestPersonKind == PersonKind.Staff ? "スタッフ" : "客";
return "不明";
}
// ---------------- スタッフ ----------------
static void DrawStaff()
{
StaffAgent[] all = Object.FindObjectsOfType<StaffAgent>();
// 同じ文で近くに立っている人はまとめる(例「スタッフ×2 待機」)
List<Transform> anchors = new List<Transform>();
List<string> texts = new List<string>();
List<int> counts = new List<int>();
for (int i = 0; i < all.Length; i++)
{
StaffAgent a = all[i];
if (a == null || a.spec == null) continue;
string text = YieldText(a.gameObject) + StaffText(a);
int at = -1;
for (int k = 0; k < anchors.Count; k++)
{
if (texts[k] != text) continue;
if (Vector3.Distance(anchors[k].position, a.transform.position) > MergeM) continue;
at = k; break;
}
if (at >= 0) { counts[at]++; continue; }
anchors.Add(a.transform); texts.Add(text); counts.Add(1);
}
for (int k = 0; k < anchors.Count; k++)
{
string head = counts[k] > 1 ? "スタッフ×" + counts[k] + " " : "スタッフ ";
Label(anchors[k], 1.7f + 0.3f, head + texts[k], TagStaff);
}
}
/// <summary>この距離の中にいる同じ文の吹き出しは、1つにまとめる(m)</summary>
const float MergeM = 1.0f;
/// <summary>
/// スタッフの字幕は仕事の名前だけにする(場所は記録に出るので書かない)。
/// 会計・案内・料理・片付け・呼び出し・待機
/// </summary>
static string StaffText(StaffAgent a)
{
if (a.CurrentJob == null) return "待機";
return StaffAgent.JobKindName(a.CurrentJob.kind);
}
// ---------------- 客 ----------------
static void DrawGuests()
{
GuestAgent[] all = Object.FindObjectsOfType<GuestAgent>();
// 待合の客は1人ずつ出さず、1つにまとめる(B5-2)
int waitingBodies = 0;
Transform waitingAnchor = null;
for (int i = 0; i < all.Length; i++)
{
GuestAgent g = all[i];
if (g == null || g.spec == null || !g.gameObject.activeInHierarchy) continue;
if (g.CurrentErrand == GuestAgent.Errand.Waiting)
{
waitingBodies++;
if (waitingAnchor == null) waitingAnchor = g.transform;
continue;
}
// ふだんは出さない。出すのは
// ・ロボットが止まった相手になったとき
// ・ロボットをよけて待っている・下がっているとき
// ・あきらめたとき
string text = GuestShortText(g);
if (text.Length == 0) continue;
Label(g.transform, g.spec.personHeight + 0.3f, text, TagGuest);
}
if (waitingAnchor != null)
{
GroupManager gm = Object.FindObjectOfType<GroupManager>();
int outside = gm != null ? Mathf.Max(0, gm.WaitingGroups - waitingBodies) : 0;
Label(waitingAnchor, 2.2f,
"待合 " + waitingBodies + "人・外で待つ " + outside + "組", TagWaiting);
}
}
/// <summary>
/// 人がロボットをよけている最中かを、字幕の前に付ける。
/// 「ロボット待ち」「よける」「回り道」。不注意の人は印を付ける。
/// </summary>
static string YieldText(GameObject go)
{
PersonRobotYield y = go != null ? go.GetComponent<PersonRobotYield>() : null;
if (y == null) return "";
string head = "";
switch (y.CurrentMode)
{
case PersonRobotYield.Mode.WaitAtEntrance:
head = "ロボット待ち" + y.ModeSeconds.ToString("0") + "s"; break;
case PersonRobotYield.Mode.BackOff: head = "よける"; break;
case PersonRobotYield.Mode.Detour: head = "回り道"; break;
}
if (y.Careless) head += "〔不注意〕";
return head.Length > 0 ? "[" + head + "] " : "";
}
/// <summary>
/// 客の字幕(最低限)。出すのは3つのときだけ。
/// 「待つ」「よける」「回り道」… ロボットをよけている
/// 「ロボットの相手」… ロボットが止まった相手になった
/// 「あきらめ」… 行き先に着けずあきらめた
/// </summary>
static string GuestShortText(GuestAgent g)
{
PersonRobotYield y = g.GetComponent<PersonRobotYield>();
if (y != null)
{
switch (y.CurrentMode)
{
case PersonRobotYield.Mode.WaitAtEntrance:
return "待つ" + y.ModeSeconds.ToString("0") + "s" + (y.Careless ? "〔不注意〕" : "");
case PersonRobotYield.Mode.BackOff:
return "よける" + (y.Careless ? "〔不注意〕" : "");
case PersonRobotYield.Mode.Detour:
return "回り道" + (y.Careless ? "〔不注意〕" : "");
}
}
if (IsRobotStopCause(g.transform)) return "ロボットの相手";
if (g.GaveUp) return "あきらめ";
return "";
}
/// <summary>その人が、いま止まっているロボットの相手になっているか</summary>
static bool IsRobotStopCause(Transform who)
{
IReadOnlyList<ServingRobotController> robots = ServingRobotController.All;
for (int i = 0; i < robots.Count; i++)
{
ServingRobotController r = robots[i];
if (r == null || r.sensor == null || r.spec == null) continue;
if (r.SpeedMps > r.spec.stoppedSpeedEpsilon) continue;
FrontSensor.Reading s = r.sensor.Last;
if (!s.blocked || !s.nearestIsPerson) continue;
if (Vector3.Distance(r.transform.position, who.position) <= r.spec.sensorRange + 0.5f)
return true;
}
return false;
}
static string GuestText(GuestAgent g)
{
string tail = g.GaveUp ? "(あきらめた)" : "";
switch (g.CurrentErrand)
{
case GuestAgent.Errand.Waiting: return "待合";
case GuestAgent.Errand.Leave: return "退店" + tail;
case GuestAgent.Errand.Pay: return "会計" + tail;
case GuestAgent.Errand.DrinkBar:
if (g.CurrentLeg == GuestAgent.Leg.Going) return "ドリンクバーへ";
if (g.CurrentLeg == GuestAgent.Leg.Dwelling) return "ドリンクバー";
return "卓へ戻る" + tail;
case GuestAgent.Errand.Toilet:
if (g.CurrentLeg == GuestAgent.Leg.Going) return "トイレへ";
if (g.CurrentLeg == GuestAgent.Leg.Dwelling) return "トイレ";
return "卓へ戻る" + tail;
}
return "";
}
static void DrawSeated()
{
SeatedGuest[] all = Object.FindObjectsOfType<SeatedGuest>();
for (int i = 0; i < all.Length; i++)
{
SeatedGuest s = all[i];
if (s == null || !s.IsPulling) continue; // 引いているときだけ出す
if (!BlamedByRobot(s.transform.position)) continue; // ロボットが止まった相手だけ
Label(s.transform, 1.3f, "椅子を引いている", TagChair);
}
}
/// <summary>
/// その卓の椅子が、いまロボットの止まった相手になっているか(B5-3)。
/// 「全員」のときも、座っている客と椅子は、これに当てはまるときだけ出す。
/// </summary>
static bool BlamedByRobot(Vector3 at)
{
IReadOnlyList<ServingRobotController> robots = ServingRobotController.All;
for (int i = 0; i < robots.Count; i++)
{
ServingRobotController r = robots[i];
if (r == null || r.spec == null || r.sensor == null) continue;
if (r.SpeedMps > r.spec.stoppedSpeedEpsilon) continue;
if (!r.sensor.Last.nearestIsChair) continue;
Vector3 d = r.transform.position - at;
d.y = 0f;
if (d.magnitude <= 2.5f) return true;
}
return false;
}
// ---------------- 点 → 場所の名前 ----------------
static string PlaceNameAt(Vector3 p)
{
if (_store == null) return "";
float best = 1.2f;
string name = "";
for (int i = 0; i < _tables.Count; i++)
{
float d = Flat(_tables[i].deliveryPoint - p);
if (d < best) { best = d; name = "卓" + _tables[i].tableId.ToString("00"); }
}
TryPlace(p, PlaceKind.FoodPass, "受け渡し口", ref best, ref name);
TryPlace(p, PlaceKind.DishReturn, "返却口", ref best, ref name);
TryPlace(p, PlaceKind.Pantry, "パントリー", ref best, ref name);
TryPlace(p, PlaceKind.Register, "レジ", ref best, ref name);
TryPlace(p, PlaceKind.Toilet, "トイレ", ref best, ref name);
TryPlace(p, PlaceKind.DrinkBar, "ドリンクバー", ref best, ref name);
TryPlace(p, PlaceKind.Entrance, "入口", ref best, ref name);
TryPlace(p, PlaceKind.Waiting, "待合", ref best, ref name);
return name;
}
static void TryPlace(Vector3 p, PlaceKind kind, string label, ref float best, ref string name)
{
if (!_store.HasPoint(kind)) return;
float d = Flat(_store.PointOf(kind) - p);
if (d < best) { best = d; name = label; }
}
static float Flat(Vector3 d) { d.y = 0f; return d.magnitude; }
}
}
Editor/ServiceSimLegend.cs
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace ServiceSim.EditorTools
{
/// <summary>
/// 真上の画面の隅に出す「色の意味」の凡例(見た目だけ)。
///
/// 字幕(ServiceSimLabels)は Scene ビューにだけ描くのでカメラに写らない。
/// 凡例はカメラに写す必要があるので、床の上に置く板と文字(TextMesh)で作る。
/// 当たりは付けないので NavMesh には焼かれない。
///
/// Unity の NavMesh の表示(通行者の中心が行ける場所)の切り替えもここに置く。
/// 見る人には「ロボットはこの細い帯しか通れない」と読めてしまうので、初期値は消す。
/// </summary>
public static class ServiceSimLegend
{
const string RootName = "FloorPaint_Legend";
const string MenuNav = "ServiceSim/Show NavMesh(中心の範囲・確認用)";
const string NavKey = "ServiceSim.ShowNavMesh";
/// <summary>
/// 真上のカメラの左上に凡例を作る。カメラの画角から位置を決めるので、
/// 店の大きさが変わってもはみ出さない。
/// </summary>
public static void Build(Transform parent, Camera topCam, float aspect,
Material robotMat, Material peopleMat)
{
Clear(parent);
if (parent == null || topCam == null || !topCam.orthographic) return;
float halfZ = topCam.orthographicSize;
float halfX = halfZ * Mathf.Max(0.01f, aspect);
Vector3 c = topCam.transform.position;
// 画面の左上から少し内側
float x = c.x - halfX + 0.35f;
float z = c.z + halfZ - 0.55f;
float y = 0.05f; // 床の塗りより上
GameObject root = new GameObject(RootName);
root.transform.SetParent(parent, false);
Row(root.transform, new Vector3(x, y, z), robotMat, "ロボットが入れる床");
Row(root.transform, new Vector3(x, y, z - 0.75f), peopleMat, "人だけが入れる床");
}
static void Row(Transform parent, Vector3 at, Material mat, string text)
{
// 色の板
GameObject chip = GameObject.CreatePrimitive(PrimitiveType.Quad);
chip.name = "FloorPaint_LegendChip";
Object.DestroyImmediate(chip.GetComponent<Collider>());
chip.transform.SetParent(parent, false);
chip.transform.position = at;
chip.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
chip.transform.localScale = new Vector3(0.55f, 0.4f, 1f);
MeshRenderer mr = chip.GetComponent<MeshRenderer>();
if (mat != null) mr.sharedMaterial = mat;
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
mr.receiveShadows = false;
// 文字
GameObject label = new GameObject("FloorPaint_LegendText");
label.transform.SetParent(parent, false);
label.transform.position = at + new Vector3(0.45f, 0f, 0f);
label.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
TextMesh tm = label.AddComponent<TextMesh>();
tm.text = text;
tm.anchor = TextAnchor.MiddleLeft;
tm.alignment = TextAlignment.Left;
tm.characterSize = 0.08f;
tm.fontSize = 56;
tm.color = new Color(0.08f, 0.09f, 0.12f);
// フォントとマテリアルは必ず入れる(入れないと何も描かれない)。
// このプロジェクトは Built-in(URP の .asset は script 参照が切れていて
// currentRenderPipeline は null)なので、フォントが持つマテリアル
// (GUI/Text Shader)がそのまま使える。念のため壊れていたら探し直す
Font font = JapaneseFont();
if (font == null) font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
if (font == null) font = Resources.GetBuiltinResource<Font>("Arial.ttf");
if (font != null) tm.font = font;
MeshRenderer lr = label.GetComponent<MeshRenderer>();
if (lr == null) return;
Material textMat = font != null ? font.material : null;
if (textMat == null || textMat.shader == null ||
textMat.shader.name == ServiceSimRender.ErrorShaderName)
{
Shader sh = Shader.Find("GUI/Text Shader");
if (sh != null && font != null)
{
textMat = new Material(sh);
textMat.mainTexture = font.material != null ? font.material.mainTexture : null;
}
}
if (textMat != null) lr.sharedMaterial = textMat;
}
static Font _font;
static bool _fontTried;
/// <summary>
/// 日本語が出るフォントを探す。組み込みの Arial には日本語が無いので、
/// OS のフォントから作る。見つからなければ組み込みのまま(文字が□になる)。
/// </summary>
static Font JapaneseFont()
{
if (_fontTried) return _font;
_fontTried = true;
string[] names =
{
"Hiragino Sans", "HiraginoSans-W3", "Hiragino Kaku Gothic ProN",
"Yu Gothic", "YuGothic", "Noto Sans CJK JP", "Arial Unicode MS"
};
try { _font = Font.CreateDynamicFontFromOSFont(names, 56); }
catch { _font = null; }
if (_font == null)
Debug.LogWarning("[ServiceSim] 凡例に使える日本語フォントが見つかりませんでした。" +
"文字が□になります(ServiceSimLegend.JapaneseFont の名前を増やしてください)");
return _font;
}
public static void Clear(Transform parent)
{
if (parent == null) return;
for (int i = parent.childCount - 1; i >= 0; i--)
{
Transform t = parent.GetChild(i);
if (t.name == RootName) Object.DestroyImmediate(t.gameObject);
}
}
// ---------------- Unity の NavMesh の表示(確認用)----------------
/// <summary>Unity の NavMesh の表示(通行者の中心が行ける場所)を出すか。初期値は消す</summary>
public static bool ShowNavMesh
{
get { return EditorPrefs.GetBool(NavKey, false); }
set { EditorPrefs.SetBool(NavKey, value); Apply(); }
}
[MenuItem(MenuNav, false, 106)]
static void Toggle()
{
ShowNavMesh = !ShowNavMesh;
Debug.Log("[ServiceSim] Unity の NavMesh の表示(中心の範囲): " +
(ShowNavMesh ? "出す" : "消す") +
"\n これは「通行者の中心が行ける場所」です。" +
"人に見せる絵の床の塗り分け(ロボットの体が入れる床)とは別物です");
}
[MenuItem(MenuNav, true)]
static bool ToggleValidate()
{
Menu.SetChecked(MenuNav, ShowNavMesh);
return true;
}
[InitializeOnLoadMethod]
static void OnLoad()
{
Apply();
EditorApplication.playModeStateChanged -= OnPlayMode;
EditorApplication.playModeStateChanged += OnPlayMode;
}
/// <summary>
/// 再生中は、Unity の NavMesh の表示(中心の範囲)を必ず消す。
/// 床の塗り分け(本体が行ける範囲)と重なって、卓のまわりに灰色の帯と
/// 三角形の線が出て見えてしまうため。Play を抜けたら設定どおりに戻す。
/// </summary>
static void OnPlayMode(PlayModeStateChange state)
{
if (state == PlayModeStateChange.EnteredPlayMode) Set(false);
else if (state == PlayModeStateChange.EnteredEditMode) Apply();
}
/// <summary>
/// Unity の NavMesh の表示を切り替える。
/// API の名前が版で変わるので、反射で探して、無ければ何もしない。
/// </summary>
static void Apply() { Set(ShowNavMesh); }
static void Set(bool on)
{
try
{
System.Type t = System.Type.GetType(
"UnityEditor.AI.NavMeshVisualizationSettings, UnityEditor");
if (t == null) return;
PropertyInfo p = t.GetProperty("showNavigation",
BindingFlags.Public | BindingFlags.Static);
if (p == null) return;
p.SetValue(null, on ? 1 : 0, null);
SceneView.RepaintAll();
}
catch
{
// 版によって無いことがある。表示の切り替えだけなので、黙って見送る
}
}
}
}
Editor/ServiceSimMenu.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
namespace ServiceSim.EditorTools
{
/// <summary>
/// エディタのメニュー ServiceSim/… 。1クリックで動くようにする。
/// 書き出すのは ExperimentPlan に書いた場所だけ。Assets/ServiceSim/ の外のアセットは変えない。
/// </summary>
public static class ServiceSimMenu
{
/// <summary>1つの店の形で測った結果</summary>
class LayoutResult
{
public string assetName;
public string kindName;
public float[] widths;
public Vector2[] outerSizes;
public List<int> tableIds = new List<int>();
public Dictionary<int, SeatKind> kinds = new Dictionary<int, SeatKind>();
public Dictionary<int, Vector3> centers = new Dictionary<int, Vector3>();
public Dictionary<int, Vector3> deliveries = new Dictionary<int, Vector3>();
public Dictionary<int, string> aisleNames = new Dictionary<int, string>();
public Dictionary<int, float[]> lengths = new Dictionary<int, float[]>();
public Dictionary<int, string[]> statuses = new Dictionary<int, string[]>();
}
/// <summary>
/// 店を組み立てて NavMesh を焼き、35卓の道のり・外寸・行き止まりの有無・
/// スクリーンショットを書き出す。格子型の店とも比べる。
/// 組み立てたものはシーンに保存しない(1クリックで作り直せるため)。
/// </summary>
[MenuItem("ServiceSim/Build Store", false, 20)]
public static void BuildStore()
{
StoreBuilder builder;
NavMeshBaker baker;
PeopleSpawner spawner;
Transform robotsRoot;
if (!OpenSceneAndFind(out builder, out baker, out spawner, out robotsRoot)) return;
StoreLayout mainLayout = builder.layout;
ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(ServiceSimSetup.ExperimentPlanPath);
RobotSpec robotSpec = AssetDatabase.LoadAssetAtPath<RobotSpec>(ServiceSimSetup.RobotSpecPath);
PeopleSpec peopleSpec = AssetDatabase.LoadAssetAtPath<PeopleSpec>(ServiceSimSetup.PeopleSpecPath);
if (mainLayout == null || plan == null || robotSpec == null || peopleSpec == null)
{
Debug.LogError("[ServiceSim] 設定ファイルが見つかりません。先に ServiceSim/Setup を実行してください");
return;
}
string shaderHow;
Shader shader = ServiceSimRender.FindOpaqueShader(out shaderHow);
if (shader == null)
{
Debug.LogError("[ServiceSim] この描画方式に合うシェーダーが見つかりません。撮らずに止めました。\n" +
ServiceSimRender.Describe() + "探した結果: " + shaderHow);
return;
}
string outDir = plan.screenshotDir;
try { Directory.CreateDirectory(outDir); }
catch (Exception e)
{
Debug.LogError("[ServiceSim] 書き出し先を作れません: " + outDir + "\n" + e.Message);
return;
}
string prefix = DateTime.Now.ToString("yyyyMMdd") + "_" + plan.runName;
int shotW = plan.screenshotWidth;
int shotH = plan.screenshotHeight;
Camera camTop = FindCamera(builder, "Cam_Top");
Camera camOblique = FindCamera(builder, "Cam_Oblique");
spawner.Clear();
ClearChildren(robotsRoot);
// ---- いまの店を、幅ごとに測る ----
LayoutResult main = Measure(builder, baker, mainLayout, camTop, shotW, shotH, outDir, prefix, true);
// ---- 行き止まり・席に入れるかの確かめ(いまの幅で)----
builder.Build();
baker.Bake();
NavMeshQueryFilter robotFilter = baker.FilterFor(baker.RobotAgentTypeId);
NavMeshQueryFilter peopleFilter = baker.FilterFor(baker.PeopleAgentTypeId);
string seatAccess = CheckSeatAccess(builder);
string regBack = CheckRegisterBack(builder, baker);
string robotPaths = CheckRobotPathsOnRobotMesh(builder, baker);
string aisleWidths =
CheckAisleWidths(builder, robotFilter, mainLayout.navAgentRadius, "ロボット用") + "\n" +
CheckAisleWidths(builder, peopleFilter, baker.PeopleRadius, "人用");
string deadEnd =
CheckNoDeadEnd(builder, baker, robotFilter, "ロボット用(半径 " +
F(mainLayout.navAgentRadius) + " m)") + "\n" +
CheckNoDeadEnd(builder, baker, peopleFilter, "人用(半径 " +
F(baker.PeopleRadius) + " m)");
// ---- 格子型の店とも比べる ----
LayoutResult grid = null;
StoreLayout gridLayout = AssetDatabase.LoadAssetAtPath<StoreLayout>(ServiceSimSetup.GridLayoutPath);
if (gridLayout != null && gridLayout != mainLayout)
{
grid = Measure(builder, baker, gridLayout, null, shotW, shotH, outDir, prefix, false);
builder.layout = mainLayout;
baker.layout = mainLayout;
}
// ---- もとに戻して、人とロボットを置く ----
builder.Build();
baker.Bake();
spawner.Spawn(0);
int robots = SpawnPreviewRobots(robotsRoot, builder, 2);
string nowLabel = "w" + Mm(mainLayout.AisleWidth);
FloorPaint.Result paint = builder.PaintFloor(baker, PersonRadius());
PlaceNameLabels.Result placeNames = PlaceNameLabels.Rebuild(builder);
if (camTop != null && builder.Plan != null)
{
ServiceSimCameras.FrameTop(camTop, builder.Plan.floor, (float)shotW / shotH);
ServiceSimLegend.Build(builder.FloorPaintRoot, camTop, (float)shotW / shotH,
builder.robotFloorMaterial, builder.peopleFloorMaterial);
Capture(camTop, shotW, shotH, Path.Combine(outDir, prefix + "_top_" + nowLabel + "_people.png"));
}
if (camOblique != null && builder.Plan != null)
{
ServiceSimCameras.FrameOblique(camOblique, builder.Plan.floor);
Capture(camOblique, shotW, shotH, Path.Combine(outDir, prefix + "_oblique_" + nowLabel + "_people.png"));
}
string overhead = CheckOverhead(builder);
// つながっていない面は、通行者の種類ごとに焼いた面を読んで調べる
NavMeshTriangulation robotTri = baker.TriangulateFor(mainLayout.navAgentRadius);
NavMeshTriangulation peopleTri = baker.TriangulateFor(baker.PeopleRadius);
string navSurface =
CheckNavMeshSurface(builder, robotTri, "ロボット用(半径 " +
F(mainLayout.navAgentRadius) + " m)", robotFilter) + "\n" +
CheckNavMeshSurface(builder, peopleTri, "人用(半径 " +
F(baker.PeopleRadius) + " m)", peopleFilter);
string noGo = CheckNoGoRooms(builder, robotTri);
// ---- 書き出し ----
WriteTableCsv(Path.Combine(outDir, prefix + "_table_path_lengths_family.csv"), main);
if (grid != null)
WriteTableCsv(Path.Combine(outDir, prefix + "_table_path_lengths_grid.csv"), grid);
WriteStoreReport(Path.Combine(outDir, prefix + "_store_report.txt"),
mainLayout, robotSpec, plan, builder, spawner, robots,
main, grid, shaderHow, overhead, deadEnd, navSurface, seatAccess,
PaintText(mainLayout, paint), aisleWidths, noGo, regBack, robotPaths,
PlaceNameText(placeNames));
TripLogger.WriteConditionsJson(
Path.Combine(outDir, prefix + "_conditions.json"),
plan.runName, mainLayout, robotSpec, peopleSpec, plan, plan.BuildConditions(mainLayout));
EditorUtility.SetDirty(mainLayout);
AssetDatabase.SaveAssets();
Debug.Log("[ServiceSim] Build Store おわり。" +
"\n 店の形: " + mainLayout.name + "(" + mainLayout.StoreKindName + ")" +
"\n 卓: " + builder.Tables.Count + " / 細い通路: " + F(mainLayout.AisleWidth) + " m" +
" / 細い通路の数: " + NarrowCount(builder) +
"\n 外寸: " + F(builder.OuterSize.x) + " × " + F(builder.OuterSize.y) + " m" +
"\n 人: スタッフ " + spawner.StaffSpawned + " / 座っている客 " + spawner.SeatedSpawned +
"\n ロボット: " + robots +
"\n シェーダー: " + shader.name +
"\n 入口まわり(ドリンクバー・待合・レジ): " +
mainLayout.FrontFitReport().Replace("\n", "\n ") +
"\n ロボットの待機場所(B2): " +
mainLayout.RobotStandbyReport().Replace("\n", "\n ") +
"\n 床の塗り分け(見た目だけ): " + PaintText(mainLayout, paint).Replace("\n", "\n ") +
"\n 場所の名前: " + PlaceNameText(placeNames) +
"\n ロボットの経路: " + robotPaths.Replace("\n", "\n ") +
"\n レジの内側に入れるか: " + regBack.Replace("\n", "\n ") +
"\n 細い通路の歩ける幅: " + aisleWidths.Replace("\n", "\n ") +
"\n 通れない場所の NavMesh: " + noGo.Replace("\n", "\n ") +
"\n 席に入れるかの確かめ: " + seatAccess.Replace("\n", "\n ") +
"\n 行き止まりの確かめ: " + deadEnd.Replace("\n", "\n ") +
"\n 真上からの見通し: " + overhead.Replace("\n", "\n ") +
"\n 焼いた NavMesh: " + navSurface.Replace("\n", "\n ") +
"\n 書き出し先: " + outDir +
"\n ※ 組み立てたものはシーンに保存していません(Build Store で作り直せます)");
}
[MenuItem("ServiceSim/Clear Built Store", false, 21)]
public static void ClearBuiltStore()
{
StoreBuilder builder;
NavMeshBaker baker;
PeopleSpawner spawner;
Transform robotsRoot;
if (!OpenSceneAndFind(out builder, out baker, out spawner, out robotsRoot)) return;
spawner.Clear();
ClearChildren(robotsRoot);
baker.Remove();
builder.Clear();
Debug.Log("[ServiceSim] 組み立てたものを消しました");
}
// ---------------- 測る ----------------
static LayoutResult Measure(StoreBuilder builder, NavMeshBaker baker, StoreLayout layout,
Camera camTop, int shotW, int shotH,
string outDir, string prefix, bool shoot)
{
builder.layout = layout;
baker.layout = layout;
LayoutResult res = new LayoutResult();
res.assetName = layout.name;
res.kindName = layout.StoreKindName;
int n = Mathf.Max(1, layout.AisleWidthCount);
res.widths = new float[n];
res.outerSizes = new Vector2[n];
int original = layout.aisleWidthIndex;
float sampleRadius = layout.navAgentRadius * 4f;
for (int wi = 0; wi < n; wi++)
{
layout.aisleWidthIndex = wi;
res.widths[wi] = layout.AisleWidth;
builder.Build();
baker.Bake();
res.outerSizes[wi] = builder.OuterSize;
Vector3 from = builder.PassPoint;
IList<StoreBuilder.TableInfo> tables = builder.Tables;
for (int i = 0; i < tables.Count; i++)
{
StoreBuilder.TableInfo t = tables[i];
if (!res.lengths.ContainsKey(t.tableId))
{
res.tableIds.Add(t.tableId);
res.lengths[t.tableId] = new float[n];
res.statuses[t.tableId] = new string[n];
}
res.kinds[t.tableId] = t.kind;
res.centers[t.tableId] = t.center;
res.deliveries[t.tableId] = t.deliveryPoint;
res.aisleNames[t.tableId] = AisleNameOf(builder, t.aisleIndex);
string status;
res.lengths[t.tableId][wi] = NavMeshBaker.PathLength(from, t.deliveryPoint, sampleRadius, out status);
res.statuses[t.tableId][wi] = status;
}
if (shoot && camTop != null && builder.Plan != null)
{
// 床の塗り分けは幅ごとに作り直す(見た目だけ。NavMesh は変えない)
builder.PaintFloor(baker, PersonRadius());
PlaceNameLabels.Rebuild(builder);
ServiceSimCameras.FrameTop(camTop, builder.Plan.floor, (float)shotW / shotH);
ServiceSimLegend.Build(builder.FloorPaintRoot, camTop, (float)shotW / shotH,
builder.robotFloorMaterial, builder.peopleFloorMaterial);
Capture(camTop, shotW, shotH,
Path.Combine(outDir, prefix + "_top_w" + Mm(res.widths[wi]) + ".png"));
}
}
layout.aisleWidthIndex = original;
res.tableIds.Sort();
return res;
}
static string AisleNameOf(StoreBuilder builder, int index)
{
if (index < 0 || index >= builder.Aisles.Count) return "";
return builder.Aisles[index].name;
}
static int NarrowCount(StoreBuilder builder)
{
int n = 0;
for (int i = 0; i < builder.Aisles.Count; i++) if (builder.Aisles[i].narrow) n++;
return n;
}
// ---------------- 行き止まりの確かめ ----------------
/// <summary>
/// 細い通路を1本ずつ、まん中でふさいでから焼き直し、
/// 「ふさいだ場所にある卓」以外のすべての卓に届くかを確かめる。
/// </summary>
static string CheckNoDeadEnd(StoreBuilder builder, NavMeshBaker baker,
NavMeshQueryFilter filter, string label)
{
StringBuilder sb = new StringBuilder();
sb.Append(label).Append('\n');
float sampleRadius = builder.layout.navAgentRadius * 4f;
Vector3 from = builder.PassPoint;
int baseFail = CountUnreachable(builder, from, sampleRadius, null, filter);
sb.Append("ふさがないとき: 届かない卓 ").Append(baseFail).Append(" / ").Append(builder.Tables.Count);
if (baseFail > 0) sb.Append(" ← 直す必要あり");
sb.Append('\n');
int tested = 0, bad = 0;
float blockLen = builder.layout.navAgentRadius * 4f; // ふさぐ長さ(m)
for (int i = 0; i < builder.Aisles.Count; i++)
{
StoreBuilder.AisleInfo a = builder.Aisles[i];
if (!a.narrow) continue;
Bounds z = a.zone;
bool northSouth = z.size.z >= z.size.x;
Vector3 center = new Vector3(z.center.x, builder.layout.wallHeight * 0.5f, z.center.z);
Vector3 size = northSouth
? new Vector3(z.size.x + 0.2f, builder.layout.wallHeight, blockLen)
: new Vector3(blockLen, builder.layout.wallHeight, z.size.z + 0.2f);
GameObject blocker = GameObject.CreatePrimitive(PrimitiveType.Cube);
blocker.name = "DeadEndTestBlocker";
blocker.transform.SetParent(
builder.NotWalkableRoots.Count > 0 ? builder.NotWalkableRoots[0] : builder.transform, true);
blocker.transform.position = center;
blocker.transform.localScale = size;
// ふさいだ箱の「侵食の影」も、ふさいだ場所として数えない
Bounds blockBounds = new Bounds(center, size);
float shadow = (builder.layout.navAgentRadius + 0.05f) * 2f;
blockBounds.size = new Vector3(blockBounds.size.x + shadow,
blockBounds.size.y,
blockBounds.size.z + shadow);
baker.Bake();
int unreachable = CountUnreachable(builder, from, sampleRadius, blockBounds, filter);
tested++;
if (unreachable > 0)
{
bad++;
sb.Append(" ").Append(a.name).Append(" をふさぐと、ふさいだ場所の外の卓 ")
.Append(unreachable).Append(" 個に届かない\n");
}
UnityEngine.Object.DestroyImmediate(blocker);
}
baker.Bake(); // もとに戻す
sb.Append("細い通路 ").Append(tested).Append(" 本を1か所ずつふさいで確かめた → ");
sb.Append(bad == 0
? "どれをふさいでも、ほかの全卓に届く(行き止まりなし)"
: bad.ToString() + " 本で届かない卓が出た");
return sb.ToString();
}
// ---------------- ロボットの経路が、ロボット用の面だけを通っているか ----------------
/// <summary>
/// 受け渡し口から全卓への経路を引き、その経路の点が
/// ぜんぶロボット用の面の上にあるかを確かめる。
/// NavMesh が2枚になったので、取りちがえていれば人用の面を通ってしまう。
/// </summary>
static string CheckRobotPathsOnRobotMesh(StoreBuilder builder, NavMeshBaker baker)
{
NavMeshQueryFilter robot = baker.FilterFor(baker.RobotAgentTypeId);
float snap = Mathf.Max(0.05f, builder.layout.navVoxelSize);
int paths = 0, corners = 0, off = 0, noPath = 0;
List<string> worst = new List<string>();
Vector3 from = builder.PassPoint;
NavMeshHit h;
if (NavMesh.SamplePosition(from, out h, builder.layout.navAgentRadius * 4f, robot))
from = h.position;
IList<StoreBuilder.TableInfo> tables = builder.Tables;
NavMeshPath path = new NavMeshPath();
for (int i = 0; i < tables.Count; i++)
{
Vector3 to = tables[i].deliveryPoint;
NavMeshHit ht;
if (NavMesh.SamplePosition(to, out ht, builder.layout.navAgentRadius * 4f, robot))
to = ht.position;
if (!NavMesh.CalculatePath(from, to, robot, path) ||
path.status != NavMeshPathStatus.PathComplete)
{
noPath++;
continue;
}
paths++;
Vector3[] c = path.corners;
for (int k = 0; k < c.Length; k++)
{
corners++;
NavMeshHit hit;
bool on = NavMesh.SamplePosition(c[k], out hit, snap, robot) &&
(new Vector2(hit.position.x - c[k].x, hit.position.z - c[k].z)).magnitude
<= snap * 0.8f;
if (on) continue;
off++;
if (worst.Count < 8)
worst.Add("T" + tables[i].tableId.ToString("00") + "(" +
F(c[k].x) + "," + F(c[k].z) + ")");
}
}
StringBuilder sb = new StringBuilder();
sb.Append("経路 ").Append(paths).Append(" 本 / 経路の点 ").Append(corners)
.Append(" 個を、ロボット用の面(半径 ").Append(F(builder.layout.navAgentRadius))
.Append(" m)で確かめた\n");
if (noPath > 0) sb.Append(" 経路が引けなかった卓: ").Append(noPath).Append("\n");
sb.Append(" ロボット用の面に乗っていない点: ").Append(off).Append(" 個");
if (off == 0) sb.Append(" → ロボットはロボット用の面だけを通っている");
else
{
sb.Append(" ← 直す必要あり\n 例: ");
for (int i = 0; i < worst.Count; i++) sb.Append(worst[i]).Append(' ');
}
return sb.ToString();
}
// ---------------- レジの内側(店員側)に入れるか ----------------
/// <summary>
/// 会計のスタッフが立つ点が、ほんとうに歩ける面の上にあるかを測る。
/// ・NavMesh.SamplePosition で別の場所に寄せられていないか(寄せられた距離を出す)
/// ・寄せられた先が店員側の中かどうか
/// ・店の奥(太い通路)から、その点まで歩いて行けるか
/// ・店員側と、その入口の歩ける幅(焼いた面で測る)
/// 人用(半径 0.2 m)とロボット用(0.25 m)の両方で出す。
/// </summary>
static string CheckRegisterBack(StoreBuilder builder, NavMeshBaker baker)
{
PlanRect back = builder.RegisterBackRect;
if (back.IsEmpty) return "レジの内側がありません(registerBackDepthZ を見る)";
Vector3 want = builder.RegisterBackPoint;
StringBuilder sb = new StringBuilder();
sb.Append("店員側: ").Append(back.ToString())
.Append("(").Append(F(back.SizeX)).Append(" × ").Append(F(back.SizeZ)).Append(" m)\n");
sb.Append(" スタッフが立つ点: (").Append(F(want.x)).Append(", ").Append(F(want.z)).Append(")\n");
sb.Append(BackFor(builder, baker.FilterFor(baker.PeopleAgentTypeId),
baker.PeopleRadius, "人用", back, want));
sb.Append(BackFor(builder, baker.FilterFor(baker.RobotAgentTypeId),
builder.layout.navAgentRadius, "ロボット用", back, want));
return sb.ToString();
}
static string BackFor(StoreBuilder builder, NavMeshQueryFilter filter, float radius,
string label, PlanRect back, Vector3 want)
{
StringBuilder sb = new StringBuilder();
sb.Append(" ").Append(label).Append("(半径 ").Append(F(radius)).Append(" m): ");
// 1. 立つ点が歩ける面の上にあるか。寄せられたら、その距離を出す
NavMeshHit hit;
if (!NavMesh.SamplePosition(want, out hit, 2.0f, filter))
{
sb.Append("立つ点のまわり 2 m に歩ける面が無い ← 入れない\n");
return sb.ToString();
}
Vector2 d = new Vector2(hit.position.x - want.x, hit.position.z - want.z);
bool inside = back.Contains(hit.position);
sb.Append("寄せられた先 (").Append(F(hit.position.x)).Append(", ").Append(F(hit.position.z))
.Append(") / 元からの距離 ").Append(F(d.magnitude)).Append(" m / ")
.Append(inside ? "店員側の中" : "店員側の外 ← 入れない");
// 2. 店の奥(太い通路)から歩いて行けるか
string status;
Vector3 from = builder.PassPoint;
float len = NavMeshBaker.PathLength(from, want, radius * 4f, filter, out status);
sb.Append(" / 受け渡し口から ").Append(len >= 0f ? F(len) + " m で行ける"
: "行けない(" + status + ") ← 入れない");
sb.Append('\n');
// 3. 店員側と入口の歩ける幅
float widthX = WalkRun(back, true, back.CenterZ, filter);
float widthZ = WalkRun(back, false, back.CenterX, filter);
float openZ = WalkRun(back, false, back.xMin + 0.03f, filter); // 西の入口のところ
sb.Append(" 歩ける幅: 東西 ").Append(F(widthX)).Append(" m / 南北 ").Append(F(widthZ))
.Append(" m / 入口(西のふち)").Append(F(openZ)).Append(" m");
if (openZ < 0.7f - 0.0005f) sb.Append(" ← 入口が 0.7 m 未満。registerBackDepthZ を広げる");
sb.Append('\n');
return sb.ToString();
}
/// <summary>
/// その線の上で、続けて歩けるいちばん長い区間(m)。
/// alongX が true なら東西に、false なら南北に走る。
/// </summary>
static float WalkRun(PlanRect r, bool alongX, float fixedValue, NavMeshQueryFilter filter)
{
const float Step = 0.02f;
float from = alongX ? r.xMin : r.zMin;
float to = alongX ? r.xMax : r.zMax;
float run = 0f, best = 0f;
for (float u = from; u <= to; u += Step)
{
Vector3 p = alongX ? new Vector3(u, 0f, fixedValue) : new Vector3(fixedValue, 0f, u);
NavMeshHit hit;
bool on = NavMesh.SamplePosition(p, out hit, Step, filter) &&
(new Vector2(hit.position.x - p.x, hit.position.z - p.z)).magnitude <= Step * 0.8f;
if (on) { run += Step; if (run > best) best = run; }
else run = 0f;
}
return best;
}
// ---------------- 細い通路の、焼いた歩ける幅 ----------------
/// <summary>
/// 細い通路1本ずつ、実際に焼いた NavMesh の歩ける幅(いちばん狭いところ)を測る。
/// 計算上の値(通路の幅 − 通行半径×2)と並べて出す。
/// 足りない通路では、その場所で左右に当たったものの名前も出す
/// (どの当たりが大きいのかを突き止めるため)。
/// </summary>
static string CheckAisleWidths(StoreBuilder builder, NavMeshQueryFilter filter,
float agentRadius, string label)
{
const float Across = 0.02f; // 幅の刻み
const float Along = 0.10f; // 長手方向の刻み
StringBuilder sb = new StringBuilder();
sb.Append(label).Append("(通行半径 ").Append(F(agentRadius)).Append(" m): ")
.Append("計算上は「通路の幅 − ").Append(F(agentRadius * 2f)).Append(" m」\n");
int bad = 0, tested = 0;
IList<StoreBuilder.AisleInfo> aisles = builder.Aisles;
for (int i = 0; i < aisles.Count; i++)
{
StoreBuilder.AisleInfo a = aisles[i];
if (!a.narrow) continue;
tested++;
Bounds z = a.zone;
bool northSouth = z.size.z >= z.size.x;
float width = northSouth ? z.size.x : z.size.z;
float want = Mathf.Max(0f, width - agentRadius * 2f);
float minRun = float.MaxValue;
Vector3 worstAt = z.center;
float alongMin = northSouth ? z.min.z : z.min.x;
float alongMax = northSouth ? z.max.z : z.max.x;
for (float t = alongMin + Along * 0.5f; t <= alongMax; t += Along)
{
float run = 0f, best = 0f;
float acrossMin = northSouth ? z.min.x : z.min.z;
float acrossMax = northSouth ? z.max.x : z.max.z;
for (float u = acrossMin; u <= acrossMax; u += Across)
{
Vector3 p = northSouth ? new Vector3(u, 0f, t) : new Vector3(t, 0f, u);
NavMeshHit hit;
bool on = NavMesh.SamplePosition(p, out hit, Across, filter) &&
(new Vector2(hit.position.x - p.x, hit.position.z - p.z)).magnitude
<= Across * 0.8f;
if (on) { run += Across; if (run > best) best = run; }
else run = 0f;
}
if (best < minRun)
{
minRun = best;
worstAt = northSouth ? new Vector3(z.center.x, 0f, t) : new Vector3(t, 0f, z.center.z);
}
}
if (minRun == float.MaxValue) minRun = 0f;
bool ng = minRun < want - 0.05f;
if (ng) bad++;
sb.Append(" ").Append(a.name).Append(": 通路 ").Append(F(width))
.Append(" m / 計算上 ").Append(F(want))
.Append(" m / 焼いた面のいちばん狭いところ ").Append(F(minRun)).Append(" m");
if (ng)
{
sb.Append(" ← ").Append(F(want - minRun)).Append(" m 足りない。")
.Append(SideHits(worstAt, northSouth));
}
sb.Append('\n');
}
sb.Append(" 細い通路 ").Append(tested).Append(" 本 → ")
.Append(bad == 0 ? "どれも計算上の幅どおり"
: bad.ToString() + " 本が計算上より狭い ← 直す必要あり");
return sb.ToString();
}
/// <summary>いちばん狭かった場所で、左右に当たったものの名前と距離</summary>
static string SideHits(Vector3 at, bool northSouth)
{
Vector3 dir = northSouth ? Vector3.right : Vector3.forward;
string left = Probe(at, -dir), right = Probe(at, dir);
return "左右にあるもの: " + left + " / " + right;
}
static string Probe(Vector3 at, Vector3 dir)
{
float[] heights = { 0.1f, 0.3f, 0.6f, 1.0f };
string best = "何もない";
float bestD = float.MaxValue;
for (int i = 0; i < heights.Length; i++)
{
RaycastHit hit;
Vector3 o = new Vector3(at.x, heights[i], at.z);
if (!Physics.Raycast(o, dir, out hit, 3f, ~0, QueryTriggerInteraction.Ignore)) continue;
if (hit.distance >= bestD) continue;
bestD = hit.distance;
best = hit.collider.transform.name + " " + F(hit.distance) + "m(y=" + F(heights[i]) + ")";
}
return best;
}
// ---------------- 入らない部屋に NavMesh が入っていないか ----------------
/// <summary>
/// 厨房・事務所のように「通れない」はずの場所に、焼いた NavMesh の面が
/// 入り込んでいないかを測る(面積で出す)。
/// 入り込んでいれば、その場所を囲む箱の当たりが足りていない。
/// </summary>
static string CheckNoGoRooms(StoreBuilder builder, NavMeshTriangulation tri)
{
StorePlan plan = builder.Plan;
if (plan == null) return "店が組み立てられていません";
int triCount = tri.indices.Length / 3;
StringBuilder sb = new StringBuilder();
int bad = 0;
for (int i = 0; i < plan.places.Count; i++)
{
PlanPlace p = plan.places[i];
if (!p.solid || p.rect.IsEmpty) continue;
float area = 0f;
for (int t = 0; t < triCount; t++)
{
Vector3 a = tri.vertices[tri.indices[t * 3]];
Vector3 b = tri.vertices[tri.indices[t * 3 + 1]];
Vector3 c = tri.vertices[tri.indices[t * 3 + 2]];
Vector3 mid = (a + b + c) / 3f;
if (!p.rect.Contains(mid)) continue;
area += Vector3.Cross(b - a, c - a).magnitude * 0.5f;
}
string nm = StoreBuilder.PlaceName(p.kind);
sb.Append(" ").Append(nm.Length > 0 ? nm : p.name)
.Append("(").Append(p.rect.ToString()).Append("): 歩ける面 ")
.Append(F(area)).Append(" m2");
if (area > 0.05f) { sb.Append(" ← 入らないはずの場所に NavMesh がある。直す必要あり"); bad++; }
sb.Append('\n');
}
sb.Append("通れない場所 ").Append(bad > 0 ? bad.ToString() + " か所に NavMesh が入り込んでいる"
: "すべてに NavMesh は入っていない");
return sb.ToString();
}
// ---------------- 席に入れるかの確かめ ----------------
/// <summary>
/// 席ごとの「人が立つ点」まで、通路から歩いて行けるかを確かめる。
/// 卓と卓のあいだのすき間が狭いと、壁側の席に人が入れない(仕切りと同じことになる)。
///
/// 焼いてある NavMesh は通行半径 navAgentRadius(ロボットに合わせた 0.25 m)で
/// 削ってあり、人(半径 0.2 m)より厳しい。ここを通れれば人も通れる。
/// 点を NavMesh に寄せる距離も navAgentRadius までにして、
/// 離れた通路へ飛び移って「通れる」ことにならないようにする。
/// </summary>
static string CheckSeatAccess(StoreBuilder builder)
{
float sampleRadius = builder.layout.navAgentRadius;
Vector3 from = builder.PassPoint;
NavMeshQueryFilter filter = PeopleNav.Filter; // 席に着くのは人
int spots = 0, bad = 0;
List<string> badNames = new List<string>();
IList<StoreBuilder.TableInfo> tables = builder.Tables;
for (int i = 0; i < tables.Count; i++)
{
StoreBuilder.TableInfo t = tables[i];
for (int s = 0; s < t.spots.Count; s++)
{
PlanSeatSpot spot = t.spots[s];
spots++;
string status;
float len = NavMeshBaker.PathLength(from, spot.point, sampleRadius, filter, out status);
if (len >= 0f) continue;
bad++;
if (badNames.Count < 24)
badNames.Add("T" + t.tableId.ToString("00") + "/" + spot.name +
"(" + F(spot.point.x) + "," + F(spot.point.z) + " " + status + ")");
}
}
StringBuilder sb = new StringBuilder();
sb.Append("人が立つ点 ").Append(spots).Append(" 個(").Append(tables.Count)
.Append(" 卓ぶん)を、通行半径 ").Append(F(sampleRadius)).Append(" m で確かめた\n");
if (bad == 0)
{
sb.Append(" すべての席に、通路から入れる");
return sb.ToString();
}
sb.Append(" 入れない席 ").Append(bad).Append(" 個 ← 直す必要あり\n ");
for (int i = 0; i < badNames.Count; i++) sb.Append(badNames[i]).Append(' ');
if (bad > badNames.Count) sb.Append("…ほか ").Append(bad - badNames.Count).Append(" 個");
return sb.ToString();
}
/// <summary>
/// 受け渡し口から届かない卓の数。
/// skip の中に停止点がある卓は、ふさいだ場所そのものなので数えない。
/// </summary>
static int CountUnreachable(StoreBuilder builder, Vector3 from, float sampleRadius, Bounds? skip,
NavMeshQueryFilter filter)
{
int bad = 0;
IList<StoreBuilder.TableInfo> tables = builder.Tables;
for (int i = 0; i < tables.Count; i++)
{
Vector3 p = tables[i].deliveryPoint;
if (skip.HasValue)
{
Bounds b = skip.Value;
if (p.x >= b.min.x && p.x <= b.max.x && p.z >= b.min.z && p.z <= b.max.z) continue;
}
string status;
if (NavMeshBaker.PathLength(from, p, sampleRadius, filter, out status) < 0f) bad++;
}
return bad;
}
// ---------------- 焼いた NavMesh を調べる ----------------
/// <summary>
/// 実際に焼けた NavMesh(NavMesh.CalculateTriangulation)を調べて、
/// 受け渡し口からつながっていない歩ける面が無いかを確かめる。
/// ベンチや卓の上面が歩ける場所になっていれば、ここに出る。
/// </summary>
static string CheckNavMeshSurface(StoreBuilder builder, NavMeshTriangulation tri, string label,
NavMeshQueryFilter filter)
{
int triCount = tri.indices.Length / 3;
if (triCount == 0) return "NavMesh の三角形が 0 です(焼けていません)";
// 同じ位置の頂点をまとめる(1 mm 刻み)
Dictionary<long, int> vmap = new Dictionary<long, int>();
int[] vid = new int[tri.vertices.Length];
for (int i = 0; i < tri.vertices.Length; i++)
{
long key = QuantKey(tri.vertices[i]);
int id;
if (!vmap.TryGetValue(key, out id)) { id = vmap.Count; vmap.Add(key, id); }
vid[i] = id;
}
float[] triArea = new float[triCount];
Vector3[] triMid = new Vector3[triCount];
Dictionary<long, List<int>> edgeMap = new Dictionary<long, List<int>>();
float total = 0f, maxY = float.MinValue;
for (int t = 0; t < triCount; t++)
{
int a = tri.indices[t * 3], b = tri.indices[t * 3 + 1], c = tri.indices[t * 3 + 2];
Vector3 pa = tri.vertices[a], pb = tri.vertices[b], pc = tri.vertices[c];
triArea[t] = Vector3.Cross(pb - pa, pc - pa).magnitude * 0.5f;
triMid[t] = (pa + pb + pc) / 3f;
total += triArea[t];
maxY = Mathf.Max(maxY, Mathf.Max(pa.y, Mathf.Max(pb.y, pc.y)));
AddEdge(edgeMap, vid[a], vid[b], t);
AddEdge(edgeMap, vid[b], vid[c], t);
AddEdge(edgeMap, vid[c], vid[a], t);
}
// 受け渡し口にいちばん近い三角形から広げる
Vector3 from = builder.PassPoint;
NavMeshHit hit;
if (NavMesh.SamplePosition(from, out hit, builder.layout.navAgentRadius * 4f, filter))
from = hit.position;
int start = -1;
float best = float.MaxValue;
for (int t = 0; t < triCount; t++)
{
float d = (triMid[t] - from).sqrMagnitude;
if (d < best) { best = d; start = t; }
}
bool[] seen = new bool[triCount];
Queue<int> q = new Queue<int>();
seen[start] = true; q.Enqueue(start);
float connected = triArea[start];
while (q.Count > 0)
{
int t = q.Dequeue();
int a = tri.indices[t * 3], b = tri.indices[t * 3 + 1], c = tri.indices[t * 3 + 2];
Walk(edgeMap, vid[a], vid[b], seen, q, triArea, ref connected);
Walk(edgeMap, vid[b], vid[c], seen, q, triArea, ref connected);
Walk(edgeMap, vid[c], vid[a], seen, q, triArea, ref connected);
}
// つながっていない面の内訳
float orphan = 0f;
float orphanMaxY = float.MinValue;
Dictionary<int, float> byArea = new Dictionary<int, float>();
List<string> worst = new List<string>();
for (int t = 0; t < triCount; t++)
{
int area = t < tri.areas.Length ? tri.areas[t] : 0;
if (!byArea.ContainsKey(area)) byArea[area] = 0f;
byArea[area] += triArea[t];
if (seen[t]) continue;
orphan += triArea[t];
orphanMaxY = Mathf.Max(orphanMaxY, triMid[t].y);
if (worst.Count < 6 && triArea[t] > 0.02f)
worst.Add("(" + F(triMid[t].x) + "," + F(triMid[t].y) + "," + F(triMid[t].z) + ") " + F(triArea[t]) + "m2");
}
StringBuilder sb = new StringBuilder();
sb.Append(label).Append(":三角形 ").Append(triCount)
.Append(" / 歩ける面 ").Append(F(total)).Append(" m2\n");
sb.Append(" 受け渡し口からつながる面: ").Append(F(connected)).Append(" m2\n");
sb.Append(" つながっていない面: ").Append(F(orphan)).Append(" m2");
if (orphan > 0.01f) sb.Append(" ← いちばん高いところ y=").Append(F(orphanMaxY)).Append(" m");
sb.Append('\n');
sb.Append(" いちばん高い歩ける面: y=").Append(F(maxY)).Append(" m\n");
sb.Append(" area の内訳: ");
foreach (KeyValuePair<int, float> kv in byArea)
sb.Append("area").Append(kv.Key).Append("=").Append(F(kv.Value)).Append("m2 ");
if (worst.Count > 0)
{
sb.Append("\n つながっていない面の例: ");
for (int i = 0; i < worst.Count; i++) sb.Append(worst[i]).Append(' ');
}
return sb.ToString();
}
static long QuantKey(Vector3 p)
{
long x = (long)Mathf.RoundToInt(p.x * 1000f) + 1048576;
long y = (long)Mathf.RoundToInt(p.y * 1000f) + 1048576;
long z = (long)Mathf.RoundToInt(p.z * 1000f) + 1048576;
return (x << 42) ^ (y << 21) ^ z;
}
static long EdgeKey(int a, int b)
{
int lo = a < b ? a : b, hi = a < b ? b : a;
return ((long)lo << 32) | (uint)hi;
}
static void AddEdge(Dictionary<long, List<int>> map, int a, int b, int tri)
{
long k = EdgeKey(a, b);
List<int> list;
if (!map.TryGetValue(k, out list)) { list = new List<int>(2); map.Add(k, list); }
list.Add(tri);
}
static void Walk(Dictionary<long, List<int>> map, int a, int b,
bool[] seen, Queue<int> q, float[] area, ref float sum)
{
List<int> list;
if (!map.TryGetValue(EdgeKey(a, b), out list)) return;
for (int i = 0; i < list.Count; i++)
{
int t = list[i];
if (seen[t]) continue;
seen[t] = true;
sum += area[t];
q.Enqueue(t);
}
}
// ---------------- 真上からの見通し ----------------
static string CheckOverhead(StoreBuilder builder)
{
Physics.SyncTransforms();
List<Vector3> points = new List<Vector3>();
points.Add(builder.PassPoint);
IList<StoreBuilder.TableInfo> tables = builder.Tables;
for (int i = 0; i < tables.Count; i++) points.Add(tables[i].deliveryPoint);
float startY = builder.layout.wallHeight + 5f;
Dictionary<string, int> firstHit = new Dictionary<string, int>();
List<string> covered = new List<string>();
int missed = 0;
for (int i = 0; i < points.Count; i++)
{
Vector3 from = new Vector3(points[i].x, startY, points[i].z);
RaycastHit hit;
if (!Physics.Raycast(from, Vector3.down, out hit, startY + 5f, ~0, QueryTriggerInteraction.Ignore))
{
missed++;
continue;
}
string name = hit.collider.transform.name;
if (!firstHit.ContainsKey(name)) firstHit[name] = 0;
firstHit[name]++;
if (hit.point.y > 0.3f) covered.Add(name + " (y=" + F(hit.point.y) + ")");
}
StringBuilder sb = new StringBuilder();
sb.Append("調べた点 ").Append(points.Count).Append("(受け渡し口+全卓の停止点)\n");
foreach (KeyValuePair<string, int> kv in firstHit)
sb.Append(" 最初に当たったもの: ").Append(kv.Key).Append(" × ").Append(kv.Value).Append('\n');
if (missed > 0) sb.Append(" 何にも当たらなかった点: ").Append(missed).Append('\n');
if (covered.Count == 0) sb.Append(" 上を覆うもの: なし(天井・屋根はありません)");
else
{
sb.Append(" 上を覆うもの: ").Append(covered.Count).Append(" 点 → ");
for (int i = 0; i < covered.Count && i < 8; i++) sb.Append(covered[i]).Append(' ');
}
return sb.ToString();
}
// ---------------- 道具 ----------------
static bool OpenSceneAndFind(out StoreBuilder builder, out NavMeshBaker baker,
out PeopleSpawner spawner, out Transform robotsRoot)
{
builder = null; baker = null; spawner = null; robotsRoot = null;
Scene active = SceneManager.GetActiveScene();
if (active.path != ServiceSimSetup.ScenePath)
{
if (!File.Exists(ServiceSimSetup.ScenePath))
{
Debug.LogError("[ServiceSim] シーンがありません。先に ServiceSim/Setup を実行してください");
return false;
}
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
Debug.Log("[ServiceSim] 取りやめました");
return false;
}
EditorSceneManager.OpenScene(ServiceSimSetup.ScenePath, OpenSceneMode.Single);
}
builder = UnityEngine.Object.FindObjectOfType<StoreBuilder>();
baker = UnityEngine.Object.FindObjectOfType<NavMeshBaker>();
spawner = UnityEngine.Object.FindObjectOfType<PeopleSpawner>();
if (builder == null || baker == null || spawner == null)
{
Debug.LogError("[ServiceSim] シーンに StoreBuilder / NavMeshBaker / PeopleSpawner が見つかりません。ServiceSim/Setup を実行してください");
return false;
}
Transform root = builder.transform.parent;
if (root != null)
{
Transform t = root.Find("Robots");
if (t == null)
{
GameObject go = new GameObject("Robots");
go.transform.SetParent(root, false);
t = go.transform;
}
robotsRoot = t;
}
else robotsRoot = builder.transform;
return true;
}
static Camera FindCamera(StoreBuilder builder, string name)
{
Transform root = builder.transform.parent;
if (root == null) return null;
Transform cams = root.Find("Cameras");
if (cams == null) return null;
Transform t = cams.Find(name);
return t != null ? t.GetComponent<Camera>() : null;
}
static void ClearChildren(Transform parent)
{
if (parent == null) return;
for (int i = parent.childCount - 1; i >= 0; i--)
UnityEngine.Object.DestroyImmediate(parent.GetChild(i).gameObject);
}
static int SpawnPreviewRobots(Transform robotsRoot, StoreBuilder builder, int count)
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(ServiceSimSetup.Root + "/Robot/ServingRobot.prefab");
if (prefab == null || robotsRoot == null) return 0;
int n = 0;
for (int i = 0; i < count; i++)
{
GameObject go = PrefabUtility.InstantiatePrefab(prefab, robotsRoot) as GameObject;
if (go == null) continue;
go.name = "ServingRobot" + (i + 1).ToString("00");
go.transform.position = builder.PassPoint + new Vector3(0f, 0f, -0.7f * i);
go.transform.rotation = Quaternion.LookRotation(Vector3.back, Vector3.up);
ServingRobotController c = go.GetComponent<ServingRobotController>();
if (c != null) c.robotId = i + 1;
n++;
}
return n;
}
/// <summary>人の半径(PeopleSpec の値。読めなければ 0.2 m)</summary>
static float PersonRadius()
{
PeopleSpec p = AssetDatabase.LoadAssetAtPath<PeopleSpec>(ServiceSimSetup.PeopleSpecPath);
return p != null ? p.personRadius : 0.2f;
}
/// <summary>床の塗り分けの報告</summary>
static string PaintText(StoreLayout layout, FloorPaint.Result r)
{
if (layout != null && !layout.showFloorPaint) return "出していません(showFloorPaint が OFF)";
if (!r.ok) return "作れませんでした: " + (string.IsNullOrEmpty(r.note) ? "-" : r.note);
float pr = PersonRadius();
return "ロボットの体が入れる床(水色): " + F(r.robotAreaM2) + " m2(" + r.robotQuads + " 枚)\n" +
"人だけが入れる床(薄い黄色): " + F(r.peopleAreaM2) + " m2(" + r.peopleQuads + " 枚)\n" +
"作り方: ロボット用 NavMesh(半径 " + F(layout.navAgentRadius) + " m)を " +
F(layout.navAgentRadius) + " m 外へ広げた範囲/" +
"人用 NavMesh(半径 " + F(pr) + " m)を " + F(pr) + " m 広げた範囲から前者を引いた範囲。" +
"マス " + F(r.cellSize) + " m\n" +
"すき間が " + F(layout.navAgentRadius * 2f) + " m より狭いところは、" +
"ロボット用 NavMesh が無いので水色にならない";
}
/// <summary>場所の名前の報告(重なりは 0 になること)</summary>
static string PlaceNameText(PlaceNameLabels.Result r)
{
return r.count + " 個(床の上の文字。真上の画面にも写る)/ " +
"重なりを避けてずらした " + r.moved + " / 場所の外に出して線でつないだ " + r.leaders +
" / 名前どうしの重なり " + r.overlaps + " 個" +
(r.overlaps == 0 ? "(重なりなし)" : " ← 直す必要あり");
}
static string Mm(float meters) { return Mathf.RoundToInt(meters * 100f).ToString("000"); }
static string F(float v) { return v.ToString("0.##", CultureInfo.InvariantCulture); }
static string KindName(SeatKind k)
{
switch (k)
{
case SeatKind.WallBooth: return "booth";
case SeatKind.WallTable: return "wall";
case SeatKind.IslandTable: return "island";
case SeatKind.Counter: return "counter";
default: return "large";
}
}
// ---------------- 書き出し ----------------
static void WriteTableCsv(string path, LayoutResult r)
{
StringBuilder sb = new StringBuilder();
sb.Append("table_id,seat_kind,aisle,table_x,table_z,delivery_x,delivery_z");
for (int i = 0; i < r.widths.Length; i++) sb.Append(",path_m_w").Append(Mm(r.widths[i]));
for (int i = 0; i < r.widths.Length; i++) sb.Append(",status_w").Append(Mm(r.widths[i]));
sb.Append('\n');
for (int i = 0; i < r.tableIds.Count; i++)
{
int id = r.tableIds[i];
sb.Append(id).Append(',')
.Append(KindName(r.kinds[id])).Append(',')
.Append(r.aisleNames.ContainsKey(id) ? r.aisleNames[id] : "").Append(',')
.Append(F(r.centers[id].x)).Append(',').Append(F(r.centers[id].z)).Append(',')
.Append(F(r.deliveries[id].x)).Append(',').Append(F(r.deliveries[id].z));
for (int w = 0; w < r.widths.Length; w++) sb.Append(',').Append(F(r.lengths[id][w]));
for (int w = 0; w < r.widths.Length; w++) sb.Append(',').Append(r.statuses[id][w]);
sb.Append('\n');
}
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
static void WriteStoreReport(string path, StoreLayout layout, RobotSpec robotSpec,
ExperimentPlan plan, StoreBuilder builder, PeopleSpawner spawner, int robots,
LayoutResult main, LayoutResult grid,
string shaderHow, string overhead, string deadEnd, string navSurface,
string seatAccess, string paintText,
string aisleWidths, string noGo, string regBack, string robotPaths,
string placeNameText)
{
StringBuilder sb = new StringBuilder();
sb.Append("ServiceSim 店の報告(v1 ファミレス型)\n");
sb.Append("作った日時: ").Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")).Append('\n');
sb.Append("Unity: ").Append(Application.unityVersion).Append('\n');
sb.Append("run_name: ").Append(plan.runName).Append("\n\n");
sb.Append("描画方式\n").Append(ServiceSimRender.Describe());
sb.Append("使うシェーダー: ").Append(shaderHow).Append("\n\n");
sb.Append("いまの設定\n");
sb.Append(" 店の形: ").Append(layout.name).Append("(").Append(layout.StoreKindName).Append(")\n");
sb.Append(" 細い通路の幅: ").Append(F(layout.AisleWidth)).Append(" m\n");
sb.Append(" 通路の譲り合い: ").Append(robotSpec.aisleYielding ? "あり" : "なし").Append('\n');
sb.Append(" 1往復の打ち切り: ").Append(F(robotSpec.maxTripSec)).Append(" 秒\n\n");
sb.Append("店の中身\n");
sb.Append(" 卓: ").Append(builder.Tables.Count).Append('\n');
int booth = 0, wall = 0, island = 0;
for (int i = 0; i < builder.Tables.Count; i++)
{
SeatKind k = builder.Tables[i].kind;
if (k == SeatKind.WallBooth) booth++;
else if (k == SeatKind.WallTable) wall++;
else if (k == SeatKind.IslandTable) island++;
}
sb.Append(" 壁際ブース ").Append(booth).Append(" / 壁沿いの卓 ").Append(wall)
.Append(" / 島の卓 ").Append(island).Append('\n');
int t2 = 0, t4 = 0, seats = 0;
for (int i = 0; i < builder.Tables.Count; i++)
{
int n = builder.Tables[i].seatCount;
seats += n;
if (n <= 2) t2++; else t4++;
}
sb.Append(" 2人席 ").Append(t2).Append(" / 4人席 ").Append(t4)
.Append(" → 席の合計 ").Append(seats).Append('\n');
sb.Append(" 通路: ").Append(builder.Aisles.Count)
.Append("(うち細い通路 ").Append(NarrowCount(builder)).Append(")\n");
for (int i = 0; i < builder.Aisles.Count; i++)
{
StoreBuilder.AisleInfo a = builder.Aisles[i];
sb.Append(" [").Append(i).Append("] ").Append(a.name)
.Append(" 幅 ").Append(F(a.width)).Append(" m")
.Append(a.narrow ? "(細い)" : "(太い・横通路)").Append('\n');
}
sb.Append(" 場所の点: ");
AppendPoint(sb, builder, PlaceKind.FoodPass, "受け渡し口");
AppendPoint(sb, builder, PlaceKind.DishReturn, "返却口");
AppendPoint(sb, builder, PlaceKind.Pantry, "パントリー");
AppendPoint(sb, builder, PlaceKind.Register, "レジ");
AppendPoint(sb, builder, PlaceKind.Toilet, "トイレ");
AppendPoint(sb, builder, PlaceKind.DrinkBar, "ドリンクバー");
AppendPoint(sb, builder, PlaceKind.Entrance, "入口");
AppendPoint(sb, builder, PlaceKind.Waiting, "待合");
sb.Append("\n\n");
sb.Append("人(普通の混み具合)\n");
sb.Append(" スタッフ ").Append(spawner.StaffSpawned)
.Append(" / 座っている客(椅子を引く仕掛け) ").Append(spawner.SeatedSpawned).Append('\n');
sb.Append(" ロボット(見た目の確認用) ").Append(robots).Append("\n\n");
string frontFit = layout.FrontFitReport();
if (frontFit.Length > 0)
sb.Append("入口まわり(ドリンクバー・待合・レジ・入口)\n ")
.Append(frontFit.Replace("\n", "\n ")).Append("\n\n");
string standby = layout.RobotStandbyReport();
if (standby.Length > 0)
sb.Append("ロボットの待機場所(B2)\n ")
.Append(standby.Replace("\n", "\n ")).Append("\n\n");
sb.Append("床の塗り分け(見た目だけ。NavMesh は変えない)\n ")
.Append(paintText.Replace("\n", "\n ")).Append("\n\n");
sb.Append("場所の名前(真上の画面に出す文字)\n ").Append(placeNameText).Append("\n\n");
sb.Append("ロボットの経路が、ロボット用の面だけを通っているか\n ")
.Append(robotPaths.Replace("\n", "\n ")).Append("\n\n");
sb.Append("レジの内側(店員側)に入れるか\n ")
.Append(regBack.Replace("\n", "\n ")).Append("\n\n");
sb.Append("細い通路の、焼いた NavMesh の歩ける幅\n ")
.Append(aisleWidths.Replace("\n", "\n ")).Append("\n\n");
sb.Append("通れない場所に NavMesh が入っていないか\n ")
.Append(noGo.Replace("\n", "\n ")).Append("\n\n");
sb.Append("席に入れるかの確かめ(通路 → 席の前)\n ")
.Append(seatAccess.Replace("\n", "\n ")).Append("\n\n");
sb.Append("行き止まりの確かめ\n ").Append(deadEnd.Replace("\n", "\n ")).Append("\n\n");
sb.Append("真上からの見通し(天井・屋根がないかの確認)\n ").Append(overhead.Replace("\n", "\n ")).Append("\n\n");
sb.Append("焼いた NavMesh のしらべ(NavMesh.CalculateTriangulation)\n ")
.Append(navSurface.Replace("\n", "\n ")).Append("\n\n");
AppendLayoutSummary(sb, main);
if (grid != null) AppendLayoutSummary(sb, grid);
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
}
static void AppendPoint(StringBuilder sb, StoreBuilder builder, PlaceKind kind, string name)
{
if (!builder.HasPoint(kind)) return;
Vector3 p = builder.PointOf(kind);
sb.Append(name).Append("(").Append(F(p.x)).Append(",").Append(F(p.z)).Append(") ");
}
static void AppendLayoutSummary(StringBuilder sb, LayoutResult r)
{
sb.Append("=== ").Append(r.assetName).Append("(").Append(r.kindName).Append(")===\n");
for (int w = 0; w < r.widths.Length; w++)
{
float min = float.MaxValue, max = float.MinValue, sum = 0f;
int ok = 0, ng = 0, inBand = 0;
for (int i = 0; i < r.tableIds.Count; i++)
{
float v = r.lengths[r.tableIds[i]][w];
if (v < 0f) { ng++; continue; }
ok++; sum += v;
if (v < min) min = v;
if (v > max) max = v;
if (v >= 6f && v <= 22f) inBand++;
}
sb.Append("細い通路 ").Append(F(r.widths[w])).Append(" m\n");
sb.Append(" 外寸: ").Append(F(r.outerSizes[w].x)).Append(" × ")
.Append(F(r.outerSizes[w].y)).Append(" m(風除室を含む)\n");
if (ok > 0)
{
sb.Append(" 道のり(受け渡し口 → 卓): いちばん短い ").Append(F(min))
.Append(" m / いちばん長い ").Append(F(max))
.Append(" m / 平均 ").Append(F(sum / ok)).Append(" m\n");
sb.Append(" LOG351 の 6〜22 m に入る卓: ").Append(inBand).Append(" / ").Append(ok).Append('\n');
}
sb.Append(" 測れた卓 ").Append(ok).Append(" / 測れなかった卓 ").Append(ng).Append('\n');
if (ng > 0)
{
sb.Append(" 測れなかった卓: ");
for (int i = 0; i < r.tableIds.Count; i++)
{
int id = r.tableIds[i];
if (r.lengths[id][w] >= 0f) continue;
sb.Append("T").Append(id).Append("(").Append(r.statuses[id][w]).Append(") ");
}
sb.Append('\n');
}
}
sb.Append('\n');
}
// ---------------- スクリーンショット ----------------
static void Capture(Camera cam, int width, int height, string path)
{
if (cam == null) return;
RenderTexture rt = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32);
rt.antiAliasing = 4;
RenderTexture prevTarget = cam.targetTexture;
bool prevEnabled = cam.enabled;
RenderTexture prevActive = RenderTexture.active;
Texture2D tex = new Texture2D(width, height, TextureFormat.RGB24, false);
try
{
cam.enabled = true;
cam.targetTexture = rt;
cam.Render();
RenderTexture.active = rt;
tex.ReadPixels(new Rect(0f, 0f, width, height), 0, 0);
tex.Apply(false);
File.WriteAllBytes(path, tex.EncodeToPNG());
// 撮れているかを調べる。
// ・ピンク(1, 0, 1)が大半 → シェーダーが描画方式に合っていない
// ・真っ暗・真っ白 → そもそも描けていない
Color32[] px = tex.GetPixels32();
long sum = 0;
int step = Mathf.Max(1, px.Length / 8192);
int n = 0, magenta = 0;
for (int i = 0; i < px.Length; i += step)
{
Color32 c = px[i];
sum += c.r + c.g + c.b;
if (c.r > 180 && c.b > 180 && c.g < 80) magenta++;
n++;
}
float mean = n > 0 ? sum / (float)(n * 3) : 0f;
float magentaRatio = n > 0 ? magenta / (float)n : 0f;
if (magentaRatio >= 0.25f)
Debug.LogError("[ServiceSim] 撮った絵の " + (magentaRatio * 100f).ToString("0") +
"% がピンク(1,0,1)です。シェーダーが描画方式に合っていません: " + path);
else if (mean < 4f || mean > 251f)
Debug.LogError("[ServiceSim] 撮った絵がほぼ一色です(平均 " + mean.ToString("0.0") +
")。描けていない可能性があります: " + path);
}
catch (Exception e)
{
Debug.LogError("[ServiceSim] スクリーンショットに失敗: " + path + "\n" + e);
}
finally
{
RenderTexture.active = prevActive;
cam.targetTexture = prevTarget;
cam.enabled = prevEnabled;
UnityEngine.Object.DestroyImmediate(tex);
rt.Release();
UnityEngine.Object.DestroyImmediate(rt);
}
}
}
}
Editor/ServiceSimRender.cs
using System;
using System.Text;
using UnityEngine;
using UnityEngine.Rendering;
namespace ServiceSim.EditorTools
{
/// <summary>
/// このプロジェクトの描画方式(レンダーパイプライン)を確かめ、それに合うシェーダーを返す。
/// 推測で名前を決めず、まずパイプラインが持つ既定のシェーダーを使う。
/// 描画方式そのものは変えない(ProjectSettings は触らない)。
/// </summary>
public static class ServiceSimRender
{
/// <summary>シェーダーが見つからないときに Unity が割り当てる名前(ピンクになる)</summary>
public const string ErrorShaderName = "Hidden/InternalErrorShader";
/// <summary>
/// 床の塗り分けに使う「透けて、光と影が乗る」シェーダーを探す。
/// このプロジェクトは Built-in なので、まず Legacy の透過拡散を使う。
/// </summary>
public static Shader FindTransparentLitShader(out string how)
{
string[] names =
{
"Legacy Shaders/Transparent/Diffuse",
"Transparent/Diffuse",
"Legacy Shaders/Transparent/VertexLit",
"Universal Render Pipeline/Lit",
"Standard"
};
for (int i = 0; i < names.Length; i++)
{
Shader sh = Shader.Find(names[i]);
if (sh == null || sh.name == ErrorShaderName) continue;
how = names[i];
return sh;
}
how = "見つからない";
return null;
}
/// <summary>いまの描画方式を、人が読める形で書き出す</summary>
public static string Describe()
{
StringBuilder sb = new StringBuilder();
RenderPipelineAsset current = GraphicsSettings.currentRenderPipeline;
RenderPipelineAsset fallback = GraphicsSettings.defaultRenderPipeline;
sb.Append("GraphicsSettings.currentRenderPipeline: ")
.Append(current == null ? "null(Built-in)" : current.name + " [" + current.GetType().Name + "]")
.Append('\n');
sb.Append("GraphicsSettings.defaultRenderPipeline: ")
.Append(fallback == null ? "null(Built-in)" : fallback.name + " [" + fallback.GetType().Name + "]")
.Append('\n');
string[] names = QualitySettings.names;
sb.Append("QualitySettings(レベルごとの上書き): ").Append(names != null ? names.Length : 0).Append(" レベル\n");
if (names != null)
{
for (int i = 0; i < names.Length; i++)
{
RenderPipelineAsset per = QualitySettings.GetRenderPipelineAssetAt(i);
sb.Append(" [").Append(i).Append("] ").Append(names[i]).Append(": ")
.Append(per == null ? "上書きなし(既定を使う)" : per.name + " [" + per.GetType().Name + "]")
.Append('\n');
}
}
sb.Append("いま選ばれている品質レベル: ").Append(QualitySettings.GetQualityLevel()).Append('\n');
sb.Append("判定: ").Append(current == null ? "Built-in" : "SRP(" + current.GetType().Name + ")").Append('\n');
if (current == null)
{
sb.Append("メモ: Built-in として扱います。URP を使うつもりなら、Graphics Settings に入れた\n");
sb.Append(" パイプラインアセットが読めていない可能性があります(スクリプト参照切れなど)。\n");
sb.Append(" ServiceSim は描画方式そのものを変えません。\n");
}
return sb.ToString();
}
/// <summary>
/// いまの描画方式に合う、ふつうの不透明シェーダーを返す。
/// 見つからなければ null を返す(呼ぶ側はマテリアルを作らずに止める)。
/// </summary>
public static Shader FindOpaqueShader(out string how)
{
RenderPipelineAsset rp = GraphicsSettings.currentRenderPipeline;
if (rp == null)
{
// Built-in
Shader s = Shader.Find("Standard");
if (IsUsable(s))
{
how = "Built-in(currentRenderPipeline が null)→ Shader.Find(\"Standard\")";
return s;
}
how = "Built-in だが Shader.Find(\"Standard\") が使えるシェーダーを返しませんでした";
return null;
}
// SRP:まずパイプライン自身が持つ既定のシェーダー(名前に頼らない)
Shader byAsset = null;
try { byAsset = rp.defaultShader; }
catch (Exception) { byAsset = null; }
if (IsUsable(byAsset))
{
how = rp.GetType().Name + ".defaultShader → \"" + byAsset.name + "\"";
return byAsset;
}
// defaultShader が取れないときだけ、方式に応じた名前で探す。
// どの方式かが分からないまま名前で当てずっぽうはしない
// (合わないシェーダーを選ぶとピンクになるため)
string typeName = rp.GetType().Name;
string[] candidates;
if (typeName.IndexOf("Universal", StringComparison.Ordinal) >= 0)
candidates = new string[] { "Universal Render Pipeline/Lit" };
else if (typeName.IndexOf("HD", StringComparison.Ordinal) >= 0)
candidates = new string[] { "HDRP/Lit" };
else
{
how = typeName + " は知らない描画方式です。合うシェーダーを推測しません";
return null;
}
for (int i = 0; i < candidates.Length; i++)
{
Shader s = Shader.Find(candidates[i]);
if (IsUsable(s))
{
how = "Shader.Find(\"" + candidates[i] + "\")";
return s;
}
}
how = typeName + " が有効だが、合うシェーダーが見つかりません(defaultShader も " +
candidates[0] + " も使えません)";
return null;
}
/// <summary>そのシェーダーで描けるか(ピンクにならないか)</summary>
public static bool IsUsable(Shader shader)
{
if (shader == null) return false;
if (shader.name == ErrorShaderName) return false;
if (!shader.isSupported) return false;
return true;
}
/// <summary>マテリアルの色・つやを、どのシェーダーでも通る名前で指定する</summary>
public static void ApplyLook(Material mat, Color color)
{
if (mat == null) return;
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color); // URP / HDRP
if (mat.HasProperty("_Color")) mat.SetColor("_Color", color); // Built-in Standard
if (mat.HasProperty("_MainColor")) mat.SetColor("_MainColor", color);
// 真上から見たときに白飛びしないように、つやを落とす
if (mat.HasProperty("_Smoothness")) mat.SetFloat("_Smoothness", 0.12f); // URP
if (mat.HasProperty("_Glossiness")) mat.SetFloat("_Glossiness", 0.12f); // Built-in
if (mat.HasProperty("_Metallic")) mat.SetFloat("_Metallic", 0f);
}
}
}
Editor/ServiceSimSetup.cs
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.SceneManagement;
namespace ServiceSim.EditorTools
{
/// <summary>
/// Assets/ServiceSim/ の中の .asset・prefab・シーンを作る。
/// すでにあるものは作り直さないが、マテリアルのシェーダーだけは毎回確かめて、
/// いまの描画方式に合わせ直す(合っていないとピンクになるため)。
/// Assets/ServiceSim/ の外のファイルは一切作らない・変えない。
/// 新しいシーンはビルド設定に足さない。
/// </summary>
public static class ServiceSimSetup
{
public const string Root = "Assets/ServiceSim";
public const string ScenePath = Root + "/Scenes/ServiceSim_Family.unity";
public const string FamilyLayoutPath = Root + "/Store/StoreLayout_Family.asset";
public const string GridLayoutPath = Root + "/Store/StoreLayout_Grid.asset";
public const string RobotSpecPath = Root + "/Robot/RobotSpec.asset";
public const string PeopleSpecPath = Root + "/People/PeopleSpec.asset";
public const string ExperimentPlanPath = Root + "/Experiment/ExperimentPlan.asset";
public const string ServiceSpecPath = Root + "/Service/ServiceSpec.asset";
const string MatDir = Root + "/Store/Materials";
const string StorePrefabDir = Root + "/Store/Prefabs";
const string OldDir = Root + "/Store/old~";
enum WalkerKind { Staff, Guest, Waiting }
static Shader _shader;
static int _materialsFixed;
[MenuItem("ServiceSim/Check Render Pipeline", false, 1)]
public static void CheckRenderPipeline()
{
string how;
Shader shader = ServiceSimRender.FindOpaqueShader(out how);
string body = ServiceSimRender.Describe() + "使うシェーダー: " + how;
if (shader == null) Debug.LogError("[ServiceSim] 描画方式に合うシェーダーが見つかりません\n" + body);
else Debug.Log("[ServiceSim] 描画方式のしらべ\n" + body);
}
[MenuItem("ServiceSim/Use Store Layout/Family (ファミレス型)", false, 30)]
public static void UseFamilyLayout() { SwitchLayout(FamilyLayoutPath); }
[MenuItem("ServiceSim/Use Store Layout/Grid (格子型・比べる用)", false, 31)]
public static void UseGridLayout() { SwitchLayout(GridLayoutPath); }
static void SwitchLayout(string path)
{
StoreLayout layout = AssetDatabase.LoadAssetAtPath<StoreLayout>(path);
if (layout == null)
{
Debug.LogError("[ServiceSim] " + path + " が読めません。先に ServiceSim/Setup を実行してください");
return;
}
StoreBuilder builder = UnityEngine.Object.FindObjectOfType<StoreBuilder>();
NavMeshBaker baker = UnityEngine.Object.FindObjectOfType<NavMeshBaker>();
if (builder == null)
{
Debug.LogError("[ServiceSim] シーンに StoreBuilder がありません。ServiceSim_Family シーンを開いてください");
return;
}
builder.layout = layout;
if (baker != null) baker.layout = layout;
EditorUtility.SetDirty(builder);
if (baker != null) EditorUtility.SetDirty(baker);
EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
Debug.Log("[ServiceSim] 店の形を切り替えました: " + layout.name + "(" + layout.StoreKindName + ")" +
"\n このあと ServiceSim/Build Store を実行してください");
}
[MenuItem("ServiceSim/Setup (アセット・prefab・シーンを作る)", false, 10)]
public static void Setup()
{
// まず描画方式を確かめる。合うシェーダーが無ければ何も作らずに止める
string how;
_shader = ServiceSimRender.FindOpaqueShader(out how);
if (_shader == null)
{
Debug.LogError("[ServiceSim] この描画方式に合うシェーダーが見つかりません。" +
"マテリアルを作らずに止めました。\n" +
ServiceSimRender.Describe() + "探した結果: " + how);
return;
}
_materialsFixed = 0;
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
Debug.Log("[ServiceSim] Setup を取りやめました");
return;
}
EnsureFolders();
// 前の版の StoreLayout_Family.asset(格子型)は種類が変わったので、
// Unity が見ないフォルダ(old~)へ寄せておく。消さない
string moved = MoveStaleFamilyLayoutAside();
Scene scene;
if (File.Exists(ScenePath))
{
scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
}
else
{
scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
EditorSceneManager.SaveScene(scene, ScenePath);
scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
}
// 設定ファイル(数字の置き場所)
BandStoreLayout family = LoadOrCreate<BandStoreLayout>(FamilyLayoutPath);
LoadOrCreate<GridStoreLayout>(GridLayoutPath);
RobotSpec robotSpec = LoadOrCreate<RobotSpec>(RobotSpecPath);
PeopleSpec peopleSpec = LoadOrCreate<PeopleSpec>(PeopleSpecPath);
ExperimentPlan plan = LoadOrCreate<ExperimentPlan>(ExperimentPlanPath);
ServiceSpec serviceSpec = LoadOrCreate<ServiceSpec>(ServiceSpecPath);
// 見た目。真上から見て、床・通路・卓・椅子・ベンチ・人・ロボットが見分けられる色にする
Material matFloor = Mat("Mat_Floor", new Color(0.86f, 0.84f, 0.79f)); // 床:明るいクリーム
Material matAisle = Mat("Mat_Aisle", new Color(0.52f, 0.58f, 0.67f)); // 通路:青灰色
Material matWall = Mat("Mat_Wall", new Color(0.40f, 0.43f, 0.48f)); // 壁・厨房・事務所
Material matBench = Mat("Mat_Bench", new Color(0.55f, 0.29f, 0.26f)); // ベンチ・仕切り:暗い赤茶
Material matTable = Mat("Mat_Table", new Color(0.86f, 0.62f, 0.30f)); // 卓:明るい木の色
Material matChair = Mat("Mat_Chair", new Color(0.13f, 0.37f, 0.42f)); // 椅子:濃い青緑
Material matCounter = Mat("Mat_Counter", new Color(0.20f, 0.22f, 0.26f)); // カウンター類:ほぼ黒
Material matRobot = Mat("Mat_Robot", new Color(0.96f, 0.96f, 0.97f)); // ロボット:白
Material matRobotFront = Mat("Mat_RobotFront", new Color(1.00f, 0.33f, 0.04f)); // ロボットの前:橙
Material matStaff = Mat("Mat_Staff", new Color(0.09f, 0.38f, 0.90f)); // スタッフ:青
Material matGuest = Mat("Mat_Guest", new Color(0.98f, 0.80f, 0.13f)); // 歩く客:黄
Material matWaiting = Mat("Mat_WaitingGuest", new Color(0.55f, 0.30f, 0.75f)); // 待合の客:紫
// 床の塗り分け(見た目だけ)。家具の色が透けて見える程度にして、光と影も乗せる
Material matRobotFloor = TransparentMat("Mat_RobotFloor", new Color(0.45f, 0.78f, 0.95f, 0.35f));
Material matPeopleFloor = TransparentMat("Mat_PeopleFloor", new Color(1.00f, 0.90f, 0.35f, 0.45f));
// 部品(prefab)。無ければ作る
EnsureBoxPrefab(StorePrefabDir + "/Floor.prefab", "Floor", matFloor, true);
EnsureBoxPrefab(StorePrefabDir + "/Wall.prefab", "Wall", matWall, true);
EnsureBoxPrefab(StorePrefabDir + "/Bench.prefab", "Bench", matBench, true);
EnsureBoxPrefab(StorePrefabDir + "/Table_Booth4.prefab", "Table_Booth4", matTable, true);
EnsureBoxPrefab(StorePrefabDir + "/KitchenPass.prefab", "KitchenPass", matCounter, true);
// 通路の色板。当たりは付けない(NavMesh に焼かれないようにする)
EnsureBoxPrefab(StorePrefabDir + "/AisleMark.prefab", "AisleMark", matAisle, false);
// ベンチの座る場所の見た目。島の卓の椅子と同じ色。
// 当たりは付けない(NavMesh・前のセンサー・左右の空きの測りに効かないようにする)
EnsureBoxPrefab(StorePrefabDir + "/BenchSeat.prefab", "BenchSeat", matChair, false);
EnsureChairPrefab(StorePrefabDir + "/Chair.prefab", matChair);
EnsureRobotPrefab(Root + "/Robot/ServingRobot.prefab", robotSpec, matRobot, matRobotFront);
EnsureWalkerPrefab(Root + "/People/Staff.prefab", "Staff", peopleSpec, matStaff, WalkerKind.Staff);
EnsureWalkerPrefab(Root + "/People/Guest.prefab", "Guest", peopleSpec, matGuest, WalkerKind.Guest);
EnsureWalkerPrefab(Root + "/People/WaitingGuest.prefab", "WaitingGuest", peopleSpec, matWaiting, WalkerKind.Waiting);
EnsureSeatedGuestPrefab(Root + "/People/SeatedGuest.prefab", peopleSpec);
// すでに作ってある prefab も直す。
// ・NavMeshAgent は切る(置く前に有効だと「NavMesh に近くないので作れません」の警告が出る)
// ・通路の色板の当たりは外す(NavMesh に焼かれないようにする)
int agentsDisabled = 0;
agentsDisabled += FixPrefab(Root + "/Robot/ServingRobot.prefab", true, false) ? 1 : 0;
agentsDisabled += FixPrefab(Root + "/People/Staff.prefab", true, false) ? 1 : 0;
agentsDisabled += FixPrefab(Root + "/People/Guest.prefab", true, false) ? 1 : 0;
agentsDisabled += FixPrefab(Root + "/People/WaitingGuest.prefab", true, false) ? 1 : 0;
int collidersRemoved = FixPrefab(StorePrefabDir + "/AisleMark.prefab", false, true) ? 1 : 0;
collidersRemoved += FixPrefab(StorePrefabDir + "/BenchSeat.prefab", false, true) ? 1 : 0;
// 人の種類(止まった原因を分けるのに使う)を、すでにある prefab にも入れ直す
int kindsFixed = 0;
kindsFixed += FixPersonKind(Root + "/People/Staff.prefab", PersonKind.Staff) ? 1 : 0;
kindsFixed += FixPersonKind(Root + "/People/Guest.prefab", PersonKind.Guest) ? 1 : 0;
kindsFixed += FixPersonKind(Root + "/People/WaitingGuest.prefab", PersonKind.Waiting) ? 1 : 0;
kindsFixed += FixPersonKind(Root + "/People/SeatedGuest.prefab", PersonKind.Seated) ? 1 : 0;
// 直したあとに読み直す(保存し直すと、前に取った参照が使えなくなるため)
AssetDatabase.Refresh();
GameObject floorPrefab = Load(StorePrefabDir + "/Floor.prefab");
GameObject wallPrefab = Load(StorePrefabDir + "/Wall.prefab");
GameObject benchPrefab = Load(StorePrefabDir + "/Bench.prefab");
GameObject tablePrefab = Load(StorePrefabDir + "/Table_Booth4.prefab");
GameObject passPrefab = Load(StorePrefabDir + "/KitchenPass.prefab");
GameObject aisleMarkPrefab = Load(StorePrefabDir + "/AisleMark.prefab");
GameObject benchSeatPrefab = Load(StorePrefabDir + "/BenchSeat.prefab");
GameObject chairPrefab = Load(StorePrefabDir + "/Chair.prefab");
GameObject robotPrefab = Load(Root + "/Robot/ServingRobot.prefab");
GameObject staffPrefab = Load(Root + "/People/Staff.prefab");
GameObject guestPrefab = Load(Root + "/People/Guest.prefab");
GameObject waitingPrefab = Load(Root + "/People/WaitingGuest.prefab");
GameObject seatedPrefab = Load(Root + "/People/SeatedGuest.prefab");
if (floorPrefab == null || wallPrefab == null || benchPrefab == null || tablePrefab == null ||
passPrefab == null || aisleMarkPrefab == null || benchSeatPrefab == null ||
chairPrefab == null || robotPrefab == null ||
staffPrefab == null || guestPrefab == null || waitingPrefab == null || seatedPrefab == null)
{
Debug.LogError("[ServiceSim] prefab を用意できませんでした。止めます");
return;
}
// シーンの組み立て(置くだけ。中身は持たない)
GameObject root = FindOrCreateRoot(scene, "ServiceSim");
GameObject storeGo = FindOrCreateChild(root.transform, "Store");
StoreBuilder builder = Ensure<StoreBuilder>(storeGo);
if (builder.layout == null) builder.layout = family;
builder.floorPrefab = floorPrefab;
builder.wallPrefab = wallPrefab;
builder.benchPrefab = benchPrefab;
builder.benchSeatPrefab = benchSeatPrefab;
builder.tablePrefab = tablePrefab;
builder.chairPrefab = chairPrefab;
builder.kitchenPassPrefab = passPrefab;
builder.aisleMarkPrefab = aisleMarkPrefab;
builder.robotFloorMaterial = matRobotFloor;
builder.peopleFloorMaterial = matPeopleFloor;
NavMeshBaker baker = Ensure<NavMeshBaker>(storeGo);
baker.layout = builder.layout;
baker.bakeRoot = storeGo.transform;
baker.store = builder;
baker.peopleRadius = peopleSpec.personRadius; // 人用の NavMesh を焼く半径
baker.registerBackArea = peopleSpec.registerBackArea; // 店員側の区画(客は入れない)
GameObject peopleGo = FindOrCreateChild(root.transform, "People");
PeopleSpawner spawner = Ensure<PeopleSpawner>(peopleGo);
spawner.spec = peopleSpec;
spawner.store = builder;
spawner.staffPrefab = staffPrefab;
spawner.seatedGuestPrefab = seatedPrefab;
spawner.service = serviceSpec;
GameObject robotsGo = FindOrCreateChild(root.transform, "Robots");
robotsGo.transform.position = Vector3.zero;
// 店の回し方(卓の空き・料理の待ち行列・仕事板)と、1組ごとの流れ
GameObject svcGo = FindOrCreateChild(root.transform, "Service");
ServiceOps opsC = Ensure<ServiceOps>(svcGo);
opsC.spec = serviceSpec;
opsC.robotSpec = robotSpec;
opsC.store = builder;
GroupManager groupsC = Ensure<GroupManager>(svcGo);
groupsC.spec = serviceSpec;
groupsC.people = peopleSpec;
groupsC.ops = opsC;
groupsC.store = builder;
groupsC.guestPrefab = guestPrefab;
groupsC.waitingGuestPrefab = waitingPrefab;
groupsC.bodyRoot = svcGo.transform;
JamWatcher jamsC = Ensure<JamWatcher>(svcGo);
jamsC.spec = peopleSpec;
jamsC.ops = opsC;
jamsC.store = builder;
spawner.ops = opsC;
GameObject expGo = FindOrCreateChild(root.transform, "Experiment");
ExperimentRunner runner = Ensure<ExperimentRunner>(expGo);
runner.plan = plan;
runner.service = serviceSpec;
runner.ops = opsC;
runner.groups = groupsC;
runner.jams = jamsC;
runner.store = builder;
runner.baker = baker;
runner.people = spawner;
runner.robotSpec = robotSpec;
runner.robotPrefab = robotPrefab;
runner.robotsRoot = robotsGo.transform;
runner.runOnPlay = false; // メニューから回すときだけ立てる
EditorUtility.SetDirty(runner);
EnsureLighting(root.transform);
EnsureCameras(root.transform);
EditorUtility.SetDirty(builder);
EditorUtility.SetDirty(baker);
EditorUtility.SetDirty(spawner);
EditorUtility.SetDirty(opsC);
EditorUtility.SetDirty(groupsC);
EditorUtility.SetDirty(jamsC);
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene, ScenePath);
// .asset に書かれていない項目を、いまの初期値で書きそろえる。
// すでに書かれている値は、読み込んだときにそのまま入っているので上書きされない。
string added = FillMissingAssetFields(new ScriptableObject[]
{
family, robotSpec, peopleSpec, plan, serviceSpec,
AssetDatabase.LoadAssetAtPath<StoreLayout>(GridLayoutPath)
});
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[ServiceSim] Setup おわり。" +
"\n .asset に書き足した項目: " + (added.Length > 0 ? added : "なし") +
"\n 描画方式: " + (CurrentPipeline() == null ? "Built-in" : CurrentPipeline().GetType().Name) +
"\n 使うシェーダー: " + _shader.name + "(" + how + ")" +
"\n シェーダーを直したマテリアル: " + _materialsFixed + " 個" +
"\n NavMeshAgent を切った prefab: " + agentsDisabled + " 個" +
"\n 当たりを外した通路板: " + collidersRemoved + " 個" +
"\n 人の種類を直した prefab: " + kindsFixed + " 個" +
(moved.Length > 0 ? "\n 前の版の店の設定を寄せました: " + moved : "") +
"\n いまの店の形: " + builder.layout.name + "(" + builder.layout.StoreKindName + ")" +
"\n 店の設定: " + FamilyLayoutPath + " / " + GridLayoutPath +
"\n シーン: " + ScenePath + "(run_name=" + plan.runName + ")" +
"\n ※ このシーンはビルド設定に足していません");
}
// ---------------- .asset の項目を書きそろえる ----------------
/// <summary>
/// .asset に書かれていない項目を、いまの初期値で書き出す。
/// Unity は .asset を読むとき、書かれていない項目に C# の初期値を入れる。
/// そのまま SetDirty して保存すると、読み込んだ値はそのまま・足りない項目だけが増える。
///
/// これをしないと、設計書の「数字はすべて .asset に置く」が守られず、
/// 実際の値が .cs の初期値になってしまう。
/// 何を書き足したかを返す(報告に出す)。
/// </summary>
static string FillMissingAssetFields(ScriptableObject[] assets)
{
List<string> added = new List<string>();
for (int i = 0; i < assets.Length; i++)
{
ScriptableObject a = assets[i];
if (a == null) continue;
string path = AssetDatabase.GetAssetPath(a);
if (string.IsNullOrEmpty(path)) continue;
HashSet<string> before = TopLevelKeys(path);
EditorUtility.SetDirty(a);
AssetDatabase.SaveAssets();
HashSet<string> after = TopLevelKeys(path);
foreach (string k in after)
if (!before.Contains(k)) added.Add(a.name + "." + k);
}
if (added.Count == 0) return "";
added.Sort();
return string.Join(", ", added.ToArray());
}
/// <summary>.asset(YAML)の、いちばん外側の項目の名前を集める</summary>
static HashSet<string> TopLevelKeys(string path)
{
HashSet<string> keys = new HashSet<string>();
if (!File.Exists(path)) return keys;
string[] lines = File.ReadAllLines(path);
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];
// MonoBehaviour の中身は半角スペース2つ下げ。さらに下げた行は中の要素なので見ない
if (!line.StartsWith(" ") || line.StartsWith(" ")) continue;
int colon = line.IndexOf(':');
if (colon <= 2) continue;
string name = line.Substring(2, colon - 2).Trim();
if (name.Length == 0 || name.StartsWith("-")) continue;
keys.Add(name);
}
return keys;
}
static UnityEngine.Rendering.RenderPipelineAsset CurrentPipeline()
{
return UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline;
}
// ---------------- 前の版の .asset を寄せる ----------------
/// <summary>
/// StoreLayout_Family.asset が前の版(格子型)のままなら、Unity が見ないフォルダへ寄せる。
/// 消さない。寄せたら、そのパスを返す。
/// </summary>
static string MoveStaleFamilyLayoutAside()
{
if (!File.Exists(FamilyLayoutPath)) return "";
if (AssetDatabase.LoadAssetAtPath<BandStoreLayout>(FamilyLayoutPath) != null) return "";
try
{
Directory.CreateDirectory(OldDir);
string dst = OldDir + "/StoreLayout_Family_v0_grid.asset";
int n = 1;
while (File.Exists(dst))
{
dst = OldDir + "/StoreLayout_Family_v0_grid_" + n.ToString() + ".asset";
n++;
}
File.Move(FamilyLayoutPath, dst);
if (File.Exists(FamilyLayoutPath + ".meta")) File.Move(FamilyLayoutPath + ".meta", dst + ".meta");
AssetDatabase.Refresh();
return dst;
}
catch (Exception e)
{
Debug.LogError("[ServiceSim] 前の版の " + FamilyLayoutPath + " を寄せられませんでした。" +
"手で消すか動かしてから、もう一度 Setup を実行してください\n" + e.Message);
return "";
}
}
// ---------------- フォルダ ----------------
static void EnsureFolders()
{
EnsureFolder("Assets", "ServiceSim");
EnsureFolder(Root, "Scenes");
EnsureFolder(Root, "Store");
EnsureFolder(Root + "/Store", "Prefabs");
EnsureFolder(Root + "/Store", "Materials");
EnsureFolder(Root, "Robot");
EnsureFolder(Root, "People");
EnsureFolder(Root, "Experiment");
EnsureFolder(Root, "Service");
EnsureFolder(Root, "Recording");
}
static void EnsureFolder(string parent, string name)
{
string path = parent + "/" + name;
if (!AssetDatabase.IsValidFolder(path)) AssetDatabase.CreateFolder(parent, name);
}
// ---------------- 設定ファイル ----------------
static T LoadOrCreate<T>(string path) where T : ScriptableObject
{
T asset = AssetDatabase.LoadAssetAtPath<T>(path);
if (asset != null) return asset;
asset = ScriptableObject.CreateInstance<T>();
AssetDatabase.CreateAsset(asset, path);
return asset;
}
// ---------------- 見た目 ----------------
/// <summary>
/// マテリアルを用意する。新しく作るときは色も付ける。
/// すでにあるときはシェーダーだけ確かめて、合っていなければ直し、
/// そのときだけ色も付け直す(手で変えた色を毎回消さないため)。
/// </summary>
static Material Mat(string name, Color color)
{
string path = MatDir + "/" + name + ".mat";
Material mat = AssetDatabase.LoadAssetAtPath<Material>(path);
if (mat == null)
{
mat = new Material(_shader);
ServiceSimRender.ApplyLook(mat, color);
AssetDatabase.CreateAsset(mat, path);
return mat;
}
bool wrong = mat.shader == null
|| mat.shader != _shader
|| mat.shader.name == ServiceSimRender.ErrorShaderName
|| !mat.shader.isSupported;
if (wrong)
{
mat.shader = _shader;
ServiceSimRender.ApplyLook(mat, color);
EditorUtility.SetDirty(mat);
_materialsFixed++;
}
return mat;
}
// ---------------- 部品 ----------------
/// <summary>
/// 透けて光が乗るマテリアルを作る(床の塗り分け用)。
/// すでにあればシェーダーと色だけ合わせ直す(止まっているときと再生中で同じ見た目にする)。
/// </summary>
static Material TransparentMat(string name, Color color)
{
string how;
Shader sh = ServiceSimRender.FindTransparentLitShader(out how);
string path = MatDir + "/" + name + ".mat";
Material mat = AssetDatabase.LoadAssetAtPath<Material>(path);
if (mat == null)
{
mat = new Material(sh != null ? sh : _shader);
AssetDatabase.CreateAsset(mat, path);
}
if (sh != null && mat.shader != sh) { mat.shader = sh; _materialsFixed++; }
mat.color = color;
if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color);
// Standard しか無いときは、透過の設定も入れる
if (mat.shader != null && mat.shader.name == "Standard")
{
mat.SetFloat("_Mode", 3f);
mat.SetOverrideTag("RenderType", "Transparent");
mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
mat.SetInt("_ZWrite", 0);
mat.DisableKeyword("_ALPHATEST_ON");
mat.EnableKeyword("_ALPHABLEND_ON");
mat.renderQueue = 3000;
}
EditorUtility.SetDirty(mat);
return mat;
}
static void EnsureBoxPrefab(string path, string name, Material mat, bool withCollider)
{
if (AssetDatabase.LoadAssetAtPath<GameObject>(path) != null) return;
GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube);
go.name = name;
SetMaterial(go, mat);
if (!withCollider)
{
Collider col = go.GetComponent<Collider>();
if (col != null) UnityEngine.Object.DestroyImmediate(col);
}
PrefabUtility.SaveAsPrefabAsset(go, path);
UnityEngine.Object.DestroyImmediate(go);
}
static void EnsureChairPrefab(string path, Material mat)
{
if (AssetDatabase.LoadAssetAtPath<GameObject>(path) != null) return;
GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube);
go.name = "Chair";
SetMaterial(go, mat);
NavMeshObstacle obstacle = go.AddComponent<NavMeshObstacle>();
obstacle.shape = NavMeshObstacleShape.Box;
obstacle.center = Vector3.zero;
obstacle.size = Vector3.one;
obstacle.carving = true;
obstacle.carveOnlyStationary = false;
obstacle.enabled = false; // 引かれているあいだだけ有効にする
go.AddComponent<Chair>();
PrefabUtility.SaveAsPrefabAsset(go, path);
UnityEngine.Object.DestroyImmediate(go);
}
static void EnsureRobotPrefab(string path, RobotSpec spec, Material body, Material front)
{
if (AssetDatabase.LoadAssetAtPath<GameObject>(path) != null) return;
float r = spec.Radius;
float h = spec.height;
GameObject go = new GameObject("ServingRobot");
GameObject bodyGo = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
bodyGo.name = "Body";
bodyGo.transform.SetParent(go.transform, false);
bodyGo.transform.localPosition = new Vector3(0f, h * 0.5f, 0f);
bodyGo.transform.localScale = new Vector3(spec.diameter, h * 0.5f, spec.diameter);
UnityEngine.Object.DestroyImmediate(bodyGo.GetComponent<Collider>());
SetMaterial(bodyGo, body);
GameObject frontGo = GameObject.CreatePrimitive(PrimitiveType.Cube);
frontGo.name = "FrontMark";
frontGo.transform.SetParent(go.transform, false);
frontGo.transform.localPosition = new Vector3(0f, h * 0.86f, r * 0.9f);
frontGo.transform.localScale = new Vector3(spec.diameter * 0.7f, 0.08f, 0.08f);
UnityEngine.Object.DestroyImmediate(frontGo.GetComponent<Collider>());
SetMaterial(frontGo, front);
CapsuleCollider col = go.AddComponent<CapsuleCollider>();
col.radius = r;
col.height = h;
col.center = new Vector3(0f, h * 0.5f, 0f);
NavMeshAgent agent = go.AddComponent<NavMeshAgent>();
agent.radius = r;
agent.height = h;
agent.speed = spec.maxSpeed;
agent.acceleration = spec.acceleration;
agent.angularSpeed = spec.turnSpeedDegPerSec;
agent.obstacleAvoidanceType = ObstacleAvoidanceType.NoObstacleAvoidance;
agent.avoidancePriority = spec.avoidancePriority;
agent.autoRepath = false;
agent.updatePosition = false;
agent.updateRotation = false;
agent.enabled = false; // 置いたあとに有効にする(警告を出さないため)
ServingRobotController controller = go.AddComponent<ServingRobotController>();
FrontSensor sensor = go.AddComponent<FrontSensor>();
sensor.spec = spec;
sensor.owner = controller;
controller.spec = spec;
controller.sensor = sensor;
PrefabUtility.SaveAsPrefabAsset(go, path);
UnityEngine.Object.DestroyImmediate(go);
}
static void EnsureWalkerPrefab(string path, string name, PeopleSpec spec, Material mat, WalkerKind kind)
{
if (AssetDatabase.LoadAssetAtPath<GameObject>(path) != null) return;
float r = spec.personRadius;
float h = spec.personHeight;
GameObject go = new GameObject(name);
GameObject bodyGo = GameObject.CreatePrimitive(PrimitiveType.Capsule);
bodyGo.name = "Body";
bodyGo.transform.SetParent(go.transform, false);
bodyGo.transform.localPosition = new Vector3(0f, h * 0.5f, 0f);
bodyGo.transform.localScale = new Vector3(r * 2f, h * 0.5f, r * 2f);
UnityEngine.Object.DestroyImmediate(bodyGo.GetComponent<Collider>());
SetMaterial(bodyGo, mat);
CapsuleCollider col = go.AddComponent<CapsuleCollider>();
col.radius = r;
col.height = h;
col.center = new Vector3(0f, h * 0.5f, 0f);
NavMeshAgent agent = go.AddComponent<NavMeshAgent>();
agent.radius = r;
agent.height = h;
agent.speed = kind == WalkerKind.Staff ? spec.staffSpeed : spec.guestSpeed;
agent.acceleration = spec.acceleration;
agent.angularSpeed = spec.angularSpeedDegPerSec;
agent.stoppingDistance = spec.stoppingDistance;
agent.obstacleAvoidanceType = spec.avoidanceQuality;
agent.avoidancePriority = spec.avoidancePriority;
agent.enabled = false; // 置いたあとに有効にする(警告を出さないため)
PersonBody personBody = go.AddComponent<PersonBody>();
personBody.radius = r;
personBody.walksInAisle = kind != WalkerKind.Waiting; // 待合に立つ客は通路を歩かない
personBody.kind = kind == WalkerKind.Staff ? PersonKind.Staff
: kind == WalkerKind.Waiting ? PersonKind.Waiting
: PersonKind.Guest;
if (kind == WalkerKind.Staff) { StaffAgent a = go.AddComponent<StaffAgent>(); a.spec = spec; }
else if (kind == WalkerKind.Guest) { GuestAgent a = go.AddComponent<GuestAgent>(); a.spec = spec; }
else { WaitingGuest a = go.AddComponent<WaitingGuest>(); a.spec = spec; }
PrefabUtility.SaveAsPrefabAsset(go, path);
UnityEngine.Object.DestroyImmediate(go);
}
static void EnsureSeatedGuestPrefab(string path, PeopleSpec spec)
{
if (AssetDatabase.LoadAssetAtPath<GameObject>(path) != null) return;
// 座っている客は、椅子を引く仕掛けだけ。見た目は持たない(計測に効かないため)
GameObject go = new GameObject("SeatedGuest");
PersonBody body = go.AddComponent<PersonBody>();
body.radius = spec.personRadius;
body.walksInAisle = false;
body.kind = PersonKind.Seated;
SeatedGuest seated = go.AddComponent<SeatedGuest>();
seated.spec = spec;
PrefabUtility.SaveAsPrefabAsset(go, path);
UnityEngine.Object.DestroyImmediate(go);
}
/// <summary>
/// すでにある prefab を直す(NavMeshAgent を切る/当たりを外す)。直したら true。
/// 中身を開いて保存し直す公式の手順を使う。
/// </summary>
static bool FixPrefab(string path, bool disableAgent, bool removeCollider)
{
if (!File.Exists(path)) return false;
GameObject contents = PrefabUtility.LoadPrefabContents(path);
if (contents == null) return false;
bool changed = false;
try
{
if (disableAgent)
{
NavMeshAgent agent = contents.GetComponent<NavMeshAgent>();
if (agent != null && agent.enabled) { agent.enabled = false; changed = true; }
}
if (removeCollider)
{
Collider[] cols = contents.GetComponentsInChildren<Collider>(true);
for (int i = 0; i < cols.Length; i++)
{
UnityEngine.Object.DestroyImmediate(cols[i]);
changed = true;
}
}
if (changed) PrefabUtility.SaveAsPrefabAsset(contents, path);
}
finally
{
PrefabUtility.UnloadPrefabContents(contents);
}
return changed;
}
/// <summary>すでにある人の prefab に、人の種類を入れ直す。直したら true</summary>
static bool FixPersonKind(string path, PersonKind kind)
{
if (!File.Exists(path)) return false;
GameObject contents = PrefabUtility.LoadPrefabContents(path);
if (contents == null) return false;
bool changed = false;
try
{
PersonBody body = contents.GetComponent<PersonBody>();
if (body != null && body.kind != kind)
{
body.kind = kind;
changed = true;
}
if (changed) PrefabUtility.SaveAsPrefabAsset(contents, path);
}
finally
{
PrefabUtility.UnloadPrefabContents(contents);
}
return changed;
}
static GameObject Load(string path)
{
return AssetDatabase.LoadAssetAtPath<GameObject>(path);
}
static void SetMaterial(GameObject go, Material mat)
{
if (mat == null) return;
Renderer rend = go.GetComponent<Renderer>();
if (rend != null) rend.sharedMaterial = mat;
}
// ---------------- シーンの骨 ----------------
static GameObject FindOrCreateRoot(Scene scene, string name)
{
GameObject[] roots = scene.GetRootGameObjects();
for (int i = 0; i < roots.Length; i++)
if (roots[i].name == name) return roots[i];
GameObject go = new GameObject(name);
SceneManager.MoveGameObjectToScene(go, scene);
return go;
}
static GameObject FindOrCreateChild(Transform parent, string name)
{
Transform t = parent.Find(name);
if (t != null) return t.gameObject;
GameObject go = new GameObject(name);
go.transform.SetParent(parent, false);
return go;
}
static T Ensure<T>(GameObject go) where T : Component
{
T c = go.GetComponent<T>();
if (c == null) c = go.AddComponent<T>();
return c;
}
static void EnsureLighting(Transform root)
{
GameObject lightsGo = FindOrCreateChild(root, "Lighting");
GameObject sunGo = FindOrCreateChild(lightsGo.transform, "Sun");
Light sun = Ensure<Light>(sunGo);
sun.type = LightType.Directional;
sun.intensity = 1.1f;
sun.shadows = LightShadows.Soft;
sun.color = new Color(1f, 0.98f, 0.94f);
sunGo.transform.rotation = Quaternion.Euler(52f, -34f, 0f);
GameObject fillGo = FindOrCreateChild(lightsGo.transform, "Fill");
Light fill = Ensure<Light>(fillGo);
fill.type = LightType.Directional;
fill.intensity = 0.35f;
fill.shadows = LightShadows.None;
fill.color = new Color(0.85f, 0.9f, 1f);
fillGo.transform.rotation = Quaternion.Euler(28f, 160f, 0f);
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat;
RenderSettings.ambientLight = new Color(0.46f, 0.47f, 0.50f);
RenderSettings.fog = false;
}
static void EnsureCameras(Transform root)
{
GameObject camsGo = FindOrCreateChild(root, "Cameras");
GameObject mainGo = FindOrCreateChild(camsGo.transform, "Main Camera");
Camera main = Ensure<Camera>(mainGo);
main.clearFlags = CameraClearFlags.SolidColor;
main.backgroundColor = new Color(0.13f, 0.14f, 0.17f);
mainGo.tag = "MainCamera";
GameObject topGo = FindOrCreateChild(camsGo.transform, "Cam_Top");
Camera top = Ensure<Camera>(topGo);
top.enabled = false;
top.clearFlags = CameraClearFlags.SolidColor;
top.backgroundColor = new Color(0.13f, 0.14f, 0.17f);
GameObject obliqueGo = FindOrCreateChild(camsGo.transform, "Cam_Oblique");
Camera oblique = Ensure<Camera>(obliqueGo);
oblique.enabled = false;
oblique.clearFlags = CameraClearFlags.SolidColor;
oblique.backgroundColor = new Color(0.13f, 0.14f, 0.17f);
// SRP が有効なときに必要な付属データを先に付けておく
EnsureSrpCameraData(mainGo);
EnsureSrpCameraData(topGo);
EnsureSrpCameraData(obliqueGo);
}
/// <summary>
/// URP / HDRP のカメラ付属データを、型名で探して付ける。
/// URP が入っていなくてもコンパイルできるように、直接の参照は持たない。
/// </summary>
static void EnsureSrpCameraData(GameObject camGo)
{
if (UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline == null) return;
string[] typeNames =
{
"UnityEngine.Rendering.Universal.UniversalAdditionalCameraData, Unity.RenderPipelines.Universal.Runtime",
"UnityEngine.Rendering.HighDefinition.HDAdditionalCameraData, Unity.RenderPipelines.HighDefinition.Runtime"
};
for (int i = 0; i < typeNames.Length; i++)
{
Type t = Type.GetType(typeNames[i]);
if (t == null) continue;
if (camGo.GetComponent(t) == null) camGo.AddComponent(t);
return;
}
}
}
}
Editor/ServiceSimStage2.cs
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace ServiceSim.EditorTools
{
/// <summary>
/// 1条件だけ回すメニュー(v2 の確かめ用)。
/// 条件と数字は ExperimentPlan の .asset。回し方だけここで決める。
/// </summary>
public static class ServiceSimStage2
{
[MenuItem("ServiceSim/Run One/1条件を回す(1コマ固定 0.05 秒)", false, 40)]
public static void RunMeasure() { Run(0f, "v3", false, 0f); }
[MenuItem("ServiceSim/Run One/1条件を回す(確かめ用 0.02 秒)", false, 41)]
public static void RunReference() { Run(-1f, "v3ref", false, 0f); }
// C1:停止点だけを変えて2回回す。ほかはすべて ExperimentPlan の trial… の値。
// Inspector を触らずにメニューから選べるようにして、出力フォルダ名に停止点を入れる
// (…_off000 と …_off015)ので、2回目で1回目が上書きされない。
// 選んだ値はその回の Job にだけ使う。StoreLayout の .asset は書き換えない
// (ExperimentRunner が回し終わりにもとの値へ戻す)。
[MenuItem("ServiceSim/Run One/停止点 0 で回す(通路の中央)", false, 50)]
public static void RunOffset000() { Run(0f, "v3", true, 0f); }
[MenuItem("ServiceSim/Run One/停止点 0.15 で回す(卓寄り・直す前と同じ)", false, 51)]
public static void RunOffset015() { Run(0f, "v3", true, 0.15f); }
// 見る用:その回だけ字幕と描画を出し、1倍速で進める。
// 設定ファイル(ExperimentPlan・StoreLayout)は書き換えない。
// 計測の比べには使わない回なので、出力フォルダの名前に _view を付ける。
[MenuItem("ServiceSim/Run One/見る用:字幕あり・描画あり(1倍速)", false, 55)]
public static void RunView() { Run(0f, "view", false, 0f, true); }
[MenuItem("ServiceSim/Run One/出力フォルダを開く", false, 60)]
public static void OpenOutput()
{
ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(ServiceSimSetup.ExperimentPlanPath);
if (plan == null) { Debug.LogError("[ServiceSim] ExperimentPlan が見つかりません"); return; }
if (!Directory.Exists(plan.trialOutputRoot))
{
Debug.LogWarning("[ServiceSim] まだありません: " + plan.trialOutputRoot);
return;
}
EditorUtility.RevealInFinder(plan.trialOutputRoot);
}
/// <summary>
/// captureOverride: 0 なら plan の計測用、-1 なら plan の確かめ用。
/// setOffset が true なら、卓の停止点を offset(m)にしてこの回だけ回す。
/// .asset は書き換えない(ExperimentRunner が回し終わりにもとの値へ戻す)。
/// false なら .asset にいま入っている値のまま回す。
/// どちらでも、出力フォルダ名には実際に使う停止点を入れる。
/// </summary>
static void Run(float captureOverride, string label, bool setOffset, float offset)
{
Run(captureOverride, label, setOffset, offset, false);
}
/// <summary>view が true なら、その回だけ字幕と描画を出して1倍速で回す(見る用)</summary>
static void Run(float captureOverride, string label, bool setOffset, float offset, bool view)
{
ExperimentRunner runner;
ExperimentPlan plan;
if (!ServiceSimRunSupport.Prepare(out runner, out plan)) return;
StoreLayout layout = CurrentLayout();
if (layout == null)
{
Debug.LogError("[ServiceSim] StoreLayout が見つかりません。ServiceSim/Setup を実行してください");
return;
}
float useOffset = setOffset ? offset : layout.deliveryOffsetFromAisleCenter;
float capture = captureOverride < 0f ? plan.referenceCaptureDeltaTime : 0f;
ExperimentRunner.Job job = new ExperimentRunner.Job();
job.aisleWidthIndex = plan.trialAisleWidthIndex;
job.crowd = plan.trialCrowd;
job.robots = plan.trialRobots;
job.seed = plan.trialSeed;
job.applyDeliveryOffset = setOffset;
job.deliveryOffset = offset;
job.folder = Path.Combine(plan.trialOutputRoot,
"one_" + plan.runName + "_" + label + "_w" + plan.trialAisleWidthIndex +
"_" + ExperimentPlan.CrowdName(plan.trialCrowd) +
"_r" + plan.trialRobots + "_s" + plan.trialSeed +
"_" + OffsetTag(useOffset) + (view ? "_view" : "") +
"_" + DateTime.Now.ToString("yyyyMMdd_HHmm"));
// フォルダ名に回し始めた時刻を入れるので、同じ条件でも上書きにならない
if (Directory.Exists(job.folder))
Debug.LogWarning("[ServiceSim] このフォルダはすでにあります。上書きになります: " + job.folder);
runner.SetSingleJob(job);
runner.timeMode = view ? ExperimentRunner.TimeMode.RealTime
: ExperimentRunner.TimeMode.FixedStep;
runner.captureDeltaTimeOverride = view ? 0f : capture;
runner.runLabel = label;
runner.viewMode = view;
runner.runOnPlay = true;
EditorUtility.SetDirty(runner);
float stepSec = capture > 0f ? capture : plan.measureCaptureDeltaTime;
float simMin = plan.warmupMinutes + plan.measureMinutes;
Debug.Log("[ServiceSim] " + (view ? "見る用に1条件を回します" : "1条件を回します") +
"(Play に入ります)。" +
(view ? "\n 見る用:字幕あり・描画あり・1倍速。設定ファイルは書き換えていません。" +
"\n 計測の比べには使いません(フォルダ名に _view が付きます)。" +
"\n 途中で Play を止めてよいです(そのときは何も書き出しません)"
: "") +
"\n 1コマ " + (view ? "実時間(1倍速)" : stepSec.ToString("0.###") + " 秒") +
"\n 店の中 " + simMin + " 分(慣らし " + plan.warmupMinutes + " 分を含む)" +
"\n 細い通路: 番号 " + plan.trialAisleWidthIndex +
" / 混み具合: " + ExperimentPlan.CrowdName(plan.trialCrowd) +
" / 台数: " + plan.trialRobots + " / 種: " + plan.trialSeed +
"\n 停止点: " + useOffset.ToString("0.###") + " m" +
(setOffset
? "(メニューが決めた値。この回だけ使い、.asset は "
+ layout.deliveryOffsetFromAisleCenter.ToString("0.###") + " m のまま)"
: "(.asset の値)") +
"\n 置き場所: " + job.folder +
"\n おわると自動で Play を抜けます");
EditorApplication.EnterPlaymode();
}
/// <summary>フォルダ名に入れる停止点(0 → off000、0.15 → off015)</summary>
static string OffsetTag(float offsetM)
{
return "off" + Mathf.RoundToInt(offsetM * 100f).ToString("000");
}
static StoreLayout CurrentLayout()
{
StoreBuilder builder = UnityEngine.Object.FindObjectOfType<StoreBuilder>();
if (builder != null && builder.layout != null) return builder.layout;
return AssetDatabase.LoadAssetAtPath<StoreLayout>(ServiceSimSetup.FamilyLayoutPath);
}
}
/// <summary>メニューから回すときの、共通の下ごしらえ</summary>
public static class ServiceSimRunSupport
{
public static bool Prepare(out ExperimentRunner runner, out ExperimentPlan plan)
{
runner = null;
plan = null;
if (EditorApplication.isPlayingOrWillChangePlaymode)
{
Debug.LogWarning("[ServiceSim] すでに Play 中です。止めてからもう一度押してください");
return false;
}
Scene active = SceneManager.GetActiveScene();
if (active.path != ServiceSimSetup.ScenePath)
{
if (!File.Exists(ServiceSimSetup.ScenePath))
{
Debug.LogError("[ServiceSim] シーンがありません。先に ServiceSim/Setup を実行してください");
return false;
}
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return false;
EditorSceneManager.OpenScene(ServiceSimSetup.ScenePath, OpenSceneMode.Single);
}
runner = UnityEngine.Object.FindObjectOfType<ExperimentRunner>();
if (runner == null)
{
Debug.LogError("[ServiceSim] シーンに ExperimentRunner がありません。ServiceSim/Setup を実行してください");
return false;
}
plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(ServiceSimSetup.ExperimentPlanPath);
if (plan == null) { Debug.LogError("[ServiceSim] ExperimentPlan が見つかりません"); return false; }
if (runner.plan == null) runner.plan = plan;
if (runner.service == null)
runner.service = AssetDatabase.LoadAssetAtPath<ServiceSpec>(ServiceSimSetup.ServiceSpecPath);
if (runner.robotPrefab == null)
runner.robotPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(
ServiceSimSetup.Root + "/Robot/ServingRobot.prefab");
if (runner.robotSpec == null)
runner.robotSpec = AssetDatabase.LoadAssetAtPath<RobotSpec>(ServiceSimSetup.RobotSpecPath);
return true;
}
/// <summary>Play を抜けたら runOnPlay を下ろす(手で Play しても回り出さないように)</summary>
[InitializeOnLoadMethod]
static void Hook()
{
EditorApplication.playModeStateChanged -= OnChanged;
EditorApplication.playModeStateChanged += OnChanged;
}
static void OnChanged(PlayModeStateChange state)
{
if (state != PlayModeStateChange.EnteredEditMode) return;
ExperimentRunner runner = UnityEngine.Object.FindObjectOfType<ExperimentRunner>();
if (runner == null) return;
if (!runner.runOnPlay && !runner.viewMode) return;
runner.runOnPlay = false;
runner.viewMode = false; // 見る用は、その回だけ
EditorUtility.SetDirty(runner);
}
}
}
Editor/ServiceSimStage3.cs
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
namespace ServiceSim.EditorTools
{
/// <summary>
/// 段階3:18条件 × 種 を、1回の Play の中で順に回す(Run All)。
/// すでに書き出してある条件は飛ばせるので、途中から再開できる。
///
/// 検査1〜3(速くした回し方でも同じ結果になるかの確かめ)もここから回す。
/// </summary>
public static class ServiceSimStage3
{
[MenuItem("ServiceSim/Stage 3/Run All(18条件 × 種)", false, 70)]
public static void RunAll()
{
ExperimentRunner runner; ExperimentPlan plan;
if (!ServiceSimRunSupport.Prepare(out runner, out plan)) return;
StoreLayout layout = UnityEngine.Object.FindObjectOfType<StoreBuilder>().layout;
List<ExperimentRunner.Job> jobs = BuildSweep(plan, layout, plan.measureCaptureDeltaTime, "");
int all = jobs.Count;
if (plan.stage3Resume) jobs = SkipDone(jobs);
if (jobs.Count == 0)
{
Debug.Log("[ServiceSim] 段階3:すべて書き出し済みです(" + all + " 条件)。" +
"やり直すときは ExperimentPlan の stage3Resume を外すか、フォルダを消してください");
return;
}
Launch(runner, plan, jobs, plan.measureCaptureDeltaTime, "stage3",
"段階3 Run All:" + jobs.Count + " / " + all + " 条件(残り)");
}
[MenuItem("ServiceSim/Stage 3/検査1(刻みだけを見る:空いている・種0)", false, 72)]
public static void Check1()
{
ExperimentRunner runner; ExperimentPlan plan;
if (!ServiceSimRunSupport.Prepare(out runner, out plan)) return;
List<ExperimentRunner.Job> jobs = new List<ExperimentRunner.Job>();
jobs.Add(One(plan, plan.trialAisleWidthIndex, CrowdLevel.Empty, plan.trialRobots, 0,
"check1_ref_002"));
jobs.Add(One(plan, plan.trialAisleWidthIndex, CrowdLevel.Empty, plan.trialRobots, 0,
"check1_fast_005"));
// 1本めは確かめ用の刻み、2本めは計測用の刻み(Run All が条件ごとに刻みを変えられないため2回に分ける)
Debug.Log("[ServiceSim] 検査1:空いている・種0 を、確かめ用の刻み " +
plan.referenceCaptureDeltaTime.ToString("0.###") + " 秒で1回、" +
"計測用の刻み " + plan.measureCaptureDeltaTime.ToString("0.###") + " 秒で1回。" +
"\n いまは確かめ用の刻みで1回だけ回します。おわったら同じメニューをもう一度押してください");
List<ExperimentRunner.Job> first = new List<ExperimentRunner.Job>();
first.Add(Directory.Exists(jobs[0].folder) ? jobs[1] : jobs[0]);
float step = Directory.Exists(jobs[0].folder) ? plan.measureCaptureDeltaTime : plan.referenceCaptureDeltaTime;
Launch(runner, plan, first, step, "check1", "検査1");
}
[MenuItem("ServiceSim/Stage 3/検査2(種5つで 0.05 を回す)", false, 73)]
public static void Check2Fast()
{
RunCheck2(false);
}
[MenuItem("ServiceSim/Stage 3/検査2(種5つで 0.02 を回す)", false, 74)]
public static void Check2Ref()
{
RunCheck2(true);
}
static void RunCheck2(bool reference)
{
ExperimentRunner runner; ExperimentPlan plan;
if (!ServiceSimRunSupport.Prepare(out runner, out plan)) return;
float step = reference ? plan.referenceCaptureDeltaTime : plan.measureCaptureDeltaTime;
string tag = reference ? "check2_ref_002" : "check2_fast_005";
List<ExperimentRunner.Job> jobs = new List<ExperimentRunner.Job>();
int[] seeds = plan.seeds != null && plan.seeds.Length > 0 ? plan.seeds : new int[] { 0, 1, 2, 3, 4 };
for (int i = 0; i < seeds.Length; i++)
jobs.Add(One(plan, plan.trialAisleWidthIndex, plan.trialCrowd, plan.trialRobots, seeds[i], tag));
if (plan.stage3Resume) jobs = SkipDone(jobs);
if (jobs.Count == 0) { Debug.Log("[ServiceSim] 検査2:すべて書き出し済みです"); return; }
Launch(runner, plan, jobs, step, tag,
"検査2(1コマ " + step.ToString("0.###") + " 秒):" + jobs.Count + " 回");
}
[MenuItem("ServiceSim/Stage 3/出力フォルダを開く", false, 90)]
public static void OpenOutput()
{
ExperimentPlan plan = AssetDatabase.LoadAssetAtPath<ExperimentPlan>(ServiceSimSetup.ExperimentPlanPath);
if (plan == null) return;
if (!Directory.Exists(plan.stage3OutputRoot))
{
Debug.LogWarning("[ServiceSim] まだありません: " + plan.stage3OutputRoot);
return;
}
EditorUtility.RevealInFinder(plan.stage3OutputRoot);
}
// ---------------- 組み立て ----------------
static List<ExperimentRunner.Job> BuildSweep(ExperimentPlan plan, StoreLayout layout,
float step, string tag)
{
List<ExperimentRunner.Job> jobs = new List<ExperimentRunner.Job>();
int[] widths = plan.aisleWidthIndices != null ? plan.aisleWidthIndices : new int[] { 0, 1, 2 };
CrowdLevel[] crowds = plan.crowdLevels != null ? plan.crowdLevels
: new CrowdLevel[] { CrowdLevel.Empty, CrowdLevel.Normal, CrowdLevel.Busy };
int[] robots = plan.robotCounts != null ? plan.robotCounts : new int[] { 1, 2 };
int[] seeds = plan.seeds != null && plan.seeds.Length > 0 ? plan.seeds : new int[] { 0 };
for (int w = 0; w < widths.Length; w++)
for (int c = 0; c < crowds.Length; c++)
for (int r = 0; r < robots.Length; r++)
for (int s = 0; s < seeds.Length; s++)
jobs.Add(One(plan, widths[w], crowds[c], robots[r], seeds[s], tag));
return jobs;
}
static ExperimentRunner.Job One(ExperimentPlan plan, int widthIndex, CrowdLevel crowd,
int robots, int seed, string tag)
{
ExperimentRunner.Job j = new ExperimentRunner.Job();
j.aisleWidthIndex = widthIndex;
j.crowd = crowd;
j.robots = robots;
j.seed = seed;
string root = tag.Length > 0 && tag.StartsWith("check")
? Path.Combine(plan.stage3OutputRoot, tag)
: Path.Combine(plan.stage3OutputRoot, DateTime.Now.ToString("yyyyMMdd") + "_" + plan.runName);
j.folder = Path.Combine(root,
"w" + widthIndex + "_" + ExperimentPlan.CrowdName(crowd) + "_r" + robots + "_s" + seed);
return j;
}
/// <summary>すでに summary.json がある条件は飛ばす(途中から再開)</summary>
static List<ExperimentRunner.Job> SkipDone(List<ExperimentRunner.Job> jobs)
{
List<ExperimentRunner.Job> left = new List<ExperimentRunner.Job>();
int skipped = 0;
for (int i = 0; i < jobs.Count; i++)
{
if (File.Exists(Path.Combine(jobs[i].folder, "summary.json"))) { skipped++; continue; }
left.Add(jobs[i]);
}
if (skipped > 0) Debug.Log("[ServiceSim] 書き出し済みを飛ばしました: " + skipped + " 条件");
return left;
}
static void Launch(ExperimentRunner runner, ExperimentPlan plan,
List<ExperimentRunner.Job> jobs, float step, string label, string what)
{
runner.SetQueue(jobs);
runner.timeMode = ExperimentRunner.TimeMode.FixedStep;
runner.captureDeltaTimeOverride = step;
runner.runLabel = label;
runner.viewMode = false; // 段階3・検査は見る用ではない
runner.runOnPlay = true;
EditorUtility.SetDirty(runner);
Debug.Log("[ServiceSim] " + what + "(Play に入ります)" +
"\n 1コマ " + step.ToString("0.###") + " 秒 / 1回あたり店の中 " +
(plan.warmupMinutes + plan.measureMinutes) + " 分" +
"\n 描画を止める: " + (plan.disableRenderingWhileRunning ? "はい" : "いいえ") +
"\n 置き場所: " + (jobs.Count > 0 ? Path.GetDirectoryName(jobs[0].folder) : "-") +
"\n おわると自動で Play を抜けます。Console の「とちゅう」で進みが見られます");
EditorApplication.EnterPlaymode();
}
}
}
記事を作るのに使ったスクリプトの全文
記事の画像・サムネイル・図解・SNS の文と、この記事の後半(出力・設計書・依頼文・コードの全文)は、Claude のクラウドの作業環境で次のスクリプトで作りました。図解(LOG357_aisle_width.html)とサムネイル(LOG357_thumbnail.html)は HTML を直接書いています。
crop_images.py(画像を切り出す)
# LOG357 の画像を、Unity のウィンドウから Scene ビューの部分だけに切り出す(元の画像は _original/ に残す)
import sys, shutil
from PIL import Image
O = sys.argv[1] # LOG357_images のフォルダ
spec = [("20261020_2212_store_grid_magenta.png", "01_magenta", (624, 281, 2029, 1124)),
("20261020_2243_store_grid_aisle.png", "02_grid_store", (593, 281, 2279, 1249)),
("20261020_2330_store_family_v1_a.png", "03_family_v1", (593, 281, 2279, 1249)),
("20261021_0645_stage2_top_view.png", "04_stage2_top", None),
("20261021_0643_stage2_register_cluster.png", "05_stage2_register_cluster", None),
("20261021_1006_wall_tables_no_access_left.png", "06_wall_tables_no_access", None),
("20261021_1024_store_v3_two_seat_tables.png", "07_two_seat_tables", (624, 281, 2216, 1467)),
("20261021_1031_people_squeeze_robot.png", "08_people_squeeze_robot", None),
("20261021_1040_register_crowd.png", "09_register_crowd", None),
("20261021_1110_navmesh_margin_a.png", "10_navmesh_center_only", None),
("20261021_1140_floorpaint_edit.png", "11_floorpaint_body", (662, 275, 2326, 1570)),
("20261021_1104_register_staff_same_side.png", "12_register_staff_same_side", None),
("20261021_1153_register_inner_no_navmesh.png", "13_register_inner_no_navmesh", None),
("20261021_1200_register_inner_staff_in.png", "14_register_inner_staff_in", None),
("20261021_1210_labels_still_vertical.png", "15_labels_still_vertical", (665, 280, 2325, 1572))]
for src, name, box in spec: # unity/screenshots/ の中で動かす
im = Image.open(src).convert("RGB")
if box:
shutil.copy(src, f"{O}/_original/LOG357_{name}.png")
im = im.crop(box)
im.save(f"{O}/LOG357_{name}.png", optimize=True)
print(name, im.size)
# LOG357_00_store_top.png は unity/screens/20261021_family_v0_top_w100.png(Build Store の真上の画面)をそのまま使った
thumb_image.py(サムネイルの店の画像を作る)
# サムネイルに入れる店の画像を作る:床を塗り分けた真上の画面から店の部分を切り、
# Unity の中の文字(スタッフ×2 待機)を床の色で消し、墨色〜紙色の白黒にして、base64 の JPEG にする
from PIL import Image, ImageOps, ImageDraw
import base64, io
im = Image.open('LOG357_images/LOG357_11_floorpaint_body.png').convert('RGB')
st = im.crop((118, 12, 1414, 1286))
d = ImageDraw.Draw(st)
d.rectangle((310, 268, 560, 318), fill=st.getpixel((300, 330)))
g = ImageOps.autocontrast(ImageOps.grayscale(st), cutoff=1)
g = g.point([int(13 + (245 - 13) * i / 255) for i in range(256)]) # 0..255 を #0d0d0d..#f5f5f0 の明るさに
g = g.resize((1000, int(g.height * 1000 / g.width)))
buf = io.BytesIO(); g.convert('RGB').save(buf, 'JPEG', quality=82)
open('thumb_plan2.b64', 'w').write(base64.b64encode(buf.getvalue()).decode())
# LOG357_thumbnail.html の <img src="data:image/jpeg;base64,..."> にこの文字列を入れた
check_render.py(サムネイルと図解を画像にして確かめる)
# サムネイルと図解を Chromium で開いて画像にし、見た目を確かめる
from playwright.sync_api import sync_playwright
import glob
exe = next(iter(glob.glob('/opt/pw-browsers/chromium-*/chrome-linux/chrome')), None)
with sync_playwright() as p:
b = p.chromium.launch(executable_path=exe) if exe else p.chromium.launch()
pg = b.new_page(viewport={'width': 1600, 'height': 900})
pg.goto('file:///mnt/user-data/outputs/LOG357/LOG357_thumbnail.html'); pg.wait_for_timeout(2600); pg.screenshot(path='th.png')
pg = b.new_page(viewport={'width': 390, 'height': 900})
pg.goto('file:///mnt/user-data/outputs/LOG357/LOG357_aisle_width.html'); pg.wait_for_timeout(800)
pg.click('#mCenter'); pg.check('#chair'); pg.wait_for_timeout(300); pg.screenshot(path='fig_m.png', full_page=True)
b.close()
assemble.py(記事の後半を組み立てる)
import os, glob, sys
U='/mnt/user-data/uploads'
IC=U+'/PixelstoMotors/デジタルツイン学習R2R2R/サービスロボット導入シミュレーション'
RUN=U+'/service_sim/unity/20261021_one_family_v0_v3_off000_before_fixes/run'
ST2=U+'/service_sim/unity/20261021_stage2_family_v0'
CODE=U+'/ServiceSim'
P='/mnt/user-data/outputs/LOG357/parts'
def rd(p): return open(p,encoding='utf-8').read().rstrip('\n')
def block(text,lang=''):
fence='````' if '```' in text else '```'
return f"{fence}{lang}\n{text}\n{fence}\n"
out=[]
H2='\n\n## '
out.append(H2+"実行した操作と出力\n")
out.append("Unity の操作は、Unity の上のメニュー ServiceSim から行います。店を組み立てるときは Setup(.asset に書かれていない項目を、いまの初期値で書きそろえる)→ Build Store(店を組み立て、NavMesh を焼き、確かめの報告と真上の画面を書き出す)の順に押します。1回だけ回すときは Run One、見るために回すときは Run One の「見る用:字幕あり・描画あり(1倍速)」を押します。\n")
out.append("\n### Build Store の報告(store_report.txt・2026-10-21 12:09:43 に書き出したもの)\n\n")
out.append(block(rd(IC+'/unity/screens/20261021_family_v0_store_report.txt')))
out.append("\n### 店の設定の書き出し(conditions.json・Build Store・2026-10-21 12:09)\n\n")
out.append(block(rd(IC+'/unity/screens/20261021_family_v0_conditions.json'),'json'))
out.append("\n### 受け渡し口から各卓までの道のり(table_path_lengths_family.csv)\n\n")
out.append(block(rd(IC+'/unity/screens/20261021_family_v0_table_path_lengths_family.csv')))
out.append("\n### 段階2:3通りの回し方の summary.json と run_info.json\n\n")
for v,lab in [('ts1','等倍'),('ts10','timeScale 10'),('fixed','1コマ固定 0.02秒')]:
out.append(f"#### {lab}(stage2_family_v0_{v})\n\n")
out.append(block(rd(f'{ST2}/stage2_family_v0_{v}/summary.json'),'json'))
out.append("\n")
out.append(block(rd(f'{ST2}/stage2_family_v0_{v}/run_info.json'),'json'))
out.append("\n")
out.append("\n### 1回目(11時3分の回・細い通路 1.0 m・普通・ロボット2台・種0・止まる点 0)の出力\n\n")
out.append("出力先は Mac の `~/ServiceSim_tmp/one_family_v0_v3_w1_normal_r2_s0_off000/` で、KIOXIA の `r2r2r-data/service_sim/unity/20261021_one_family_v0_v3_off000_before_fixes/run/` にコピーし、sha256 とバイト数が10ファイルとも一致することを確かめています。\n\n")
for n,lang in [('summary.json','json'),('run_info.json','json'),('conditions.json','json'),('trips.csv',''),('stops.csv',''),('groups.csv',''),('dishes.csv',''),('give_ups.csv',''),('contacts.csv',''),('people_jams.csv','')]:
out.append(f"#### {n}\n\n")
out.append(block(rd(f'{RUN}/{n}'),lang)); out.append("\n")
out.append("\n### 試算(SimPy)を同じ条件で回したコマンドと出力\n\n")
out.append("haizen_sim.py と同じフォルダで、標準ファミレス・スタッフ2人・ロボット2台・35組/時・料理を運ぶだけ・最大3卓・壊れ物の使い分けなしを、40回の平均で回しました(simpy 4.1.1・Python 3)。\n\n")
out.append(block(open('/tmp/claude-0/build/simpy_cmd.txt',encoding='utf-8').read().rstrip('\n'),'bash'))
out.append("\n### 仮の値を1つずつ振った確かめ(simulation/sensitivity/)\n\n")
out.append("#### README.md\n\n"+block(rd(IC+'/simulation/sensitivity/README.md'),'markdown'))
out.append("\n#### sweep.py\n\n"+block(rd(IC+'/simulation/sensitivity/sweep.py'),'python'))
out.append("\n#### sweep.json\n\n"+block(rd(IC+'/simulation/sensitivity/sweep.json'),'json'))
out.append("\n#### haizen_sim.py(試算の本体・LOG351 のコード)\n\n"+block(rd(IC+'/simulation/haizen_sim.py'),'python'))
out.append(H2+"設計書と Claude Code への依頼文の全文\n")
out.append("設計書は Claude が書き、依頼文は設計書をもとに Claude が書いて、Claude Code に渡しました。直しの途中で Claude Code が書き足した部分(v3 の「済み」の表や、B4・B7 で分かったこと)も、そのまま載せています。\n")
for n,lab in [('unity_design_v0.md','設計書 unity_design_v0.md'),('claude_code_request_v0.md','依頼文 v0(段階1〜4)'),('claude_code_request_v1_store.md','依頼文 v1(ファミレス型の店)'),('claude_code_request_v2_guests.md','依頼文 v2(お客さんの流れ)'),('claude_code_request_v3_fixes.md','依頼文 v3(1回目のあとの直し)')]:
out.append(f"\n### {lab}\n\n"+block(rd(f'{IC}/unity/{n}'),'markdown'))
out.append(H2+"この回に使ったコードの全文\n")
out.append("Mac の `/Users/shimazueiichi/Robotics_UX/Robotics_UX_New/Assets/ServiceSim/` の中身です(2026-10-21 12時すぎ、区切った時点のもの。場所の名前と警告が直っていない状態)。シーンファイル(ServiceSim_Family.unity)と、使っていない古い設定(Store/old~/)は載せていません。Unity 2022.3.62f3 で動かしています。\n")
files=sorted(glob.glob(CODE+'/**/*',recursive=True))
order=['Store','Robot','People','Service','Experiment','Editor']
files=[f for f in files if os.path.isfile(f) and f.endswith(('.cs','.asset'))]
files.sort(key=lambda f:(order.index(f[len(CODE)+1:].split('/')[0]), f))
for f in files:
rel=f[len(CODE)+1:]
lang='csharp' if f.endswith('.cs') else 'yaml'
out.append(f"\n### {rel}\n\n"+block(rd(f),lang))
open(P+'/02_tail.md','w',encoding='utf-8').write(''.join(out))
print(len(files))
sns.py(SNS の文を txt と json に書き出す)
import json
url="https://pixelstomotors.com/record/unity-family-restaurant-serving-robot"
title="サービスロボット導入シミュレーション Log#4 | Unity にファミレスを建て、配膳ロボットを走らせた"
meta="Unityに35卓のファミレスを建て、配膳ロボット2台とスタッフ2人、入店から退店までの客を動かした。間取りを5回直した1回目では、往復の88%でロボットが60秒以上動けず、原因はロボットより店と人の動きの側にあった。"
tags=["サービスロボット導入シミュレーション","配膳ロボット","Unity","NavMesh","ファミレス","シミュレーション","Claude Code","SimPy","人手不足"]
x="Unityにファミレスを建てて配膳ロボットを走らせたら、往復の88%で60秒以上動けなくなった。原因はロボットより、椅子を引くお客さんとレジの作り。店が本物っぽくなるまで数字は使えない #R2R2R #PixelsToMotors\n"+url
fb="""配膳ロボットって、お店の中で何回くらい止まってるんだろう。前に配膳ロボットで店の人を1人減らせるかを試算したとき、ここだけは仮の数字で置いていました。お店の間取りがないと計算できないからです。
なので今回は、Unity の中にファミレスを1軒建てました。卓35・席122、厨房とパントリーとレジとトイレもあります。そこに配膳ロボット2台とスタッフ2人、入ってきて食べて会計して帰るお客さんを動かしました。コードは Claude Code に書いてもらって、自分は画面を見て「この店はおかしい」と言う役です。
これが思ったより大変で、間取りを5回くらい作り直しました。壁ぞいの奥の席に人が入れない。レジのまわりに16人固まる。お客さんが自分からロボットにぶつかっていく。実際のお店ではありえないことが、画面で見ると次々出てきます。
で、ひととおり整えて2時間分を回したら、ロボットの往復の88%で、60秒以上動けなくなりました。料理が届くまでは平均12.7分。試算では0.67分だったのに。
原因を追うと、ロボットの性能じゃなくて、お客さんが椅子を引いて通路にはみ出すのが497回、とかの店の側でした。椅子をどれくらい引くかも仮の数字なんだよね。店と人が本物っぽくなるまでは、この数字は使えないなと思いました。
もうひとつ面白かったのが床の色。Unity の地図は「ロボットの中心が行ける範囲」で出るので、細い通路が1本の線に見えるんです。これだと見た人は「この線しか通れない」と思っちゃう。ロボットの体が入れる範囲で塗り直しました。
動画も5本載せています。止まる回数をちゃんと測るのは次の回です。"""
li_en="""How often does a serving robot stop inside a restaurant, and for how long? When I estimated whether serving robots could replace one staff member, I had to guess those numbers. They depend on the floor plan, and I had none.
So I built a family restaurant inside Unity: 35 tables, 122 seats, a kitchen, a pantry, a register and restrooms. Two serving robots, two staff, and guests who come in, wait, sit, eat, pay and leave. Claude Code wrote the code. My job was to watch the screen and say "this is not how a real restaurant works."
That took about five rebuilds. Guests could not reach the inner seats along the wall. Sixteen people piled up at the register. Guests walked straight into robots, which people in a real restaurant don't do unless it is on purpose or they are kids.
After the store looked reasonable, I ran two hours of service. In 76 of 86 robot trips (88%), the robot was blocked for 60 seconds or more. Food took 12.7 minutes on average from kitchen to table. My earlier estimate said 0.67 minutes.
The cause was mostly the store, not the robot. Guests pulled their chairs into the aisle 497 times, and how far and how long they pull is still a placeholder. Until the store and the people behave like real ones, the robot numbers can't go back into the estimate.
One more thing I changed: the floor color. Unity's navigation map shows where the robot's center can go, so a narrow aisle looks like a thin line. Anyone watching would think the robot can only use that line. I repainted the floor to show where the robot's body fits.
Measuring the stops condition by condition comes next."""
li_ja="""配膳ロボットは、お店の中で何回止まり、何秒待たされるのか。配膳ロボットで店の人を1人減らせるかを試算したとき、ここは仮の数字で置くしかありませんでした。間取りがないと計算できないからです。
そこで Unity の中に、ファミリーレストランを1軒建てました。卓35・席122で、厨房・パントリー・レジ・トイレもあります。配膳ロボット2台とスタッフ2人、それに入ってきて、待って、座って、食べて、会計して帰るお客さんを動かしています。コードは Claude Code が書き、自分は画面を見て「実際の店ではこうならない」と言う役でした。
間取りは5回ほど作り直しました。壁ぞいの奥の席にお客さんが入れない。レジのまわりに16人が固まる。お客さんが自分からロボットに向かって歩いていく。実際の店で、わざとか子ども以外はしないことです。
店の形が整ったところで、2時間分を回しました。ロボットの往復86回のうち76回(88%)で、60秒以上動けなくなりました。料理ができてから席に届くまでは平均12.7分。前の試算では0.67分でした。
原因はロボットより、店の側にありました。お客さんが椅子を引いて通路にはみ出したのが497回。椅子をどれだけ、何秒引くかは、まだ仮の値です。店と人が本物らしく動くまでは、ロボットの数字を試算に戻せません。
もう1つ、床の色も変えました。Unity の地図は「ロボットの中心が行ける範囲」で出るので、細い通路は細い線に見えます。見た人は、ロボットがその線しか通れないと思うはずです。ロボットの体が入れる範囲で塗り直しました。
条件ごとに止まる回数を測るのは、次の回です。"""
lc_en="""Notes on the setup: the robot is a 0.5 m cylinder with a 0.6 m front sensor rule (stop if something is within 0.6 m, move again after 1 s of clear space, look for another route after 10 s, count as stuck after 60 s). Unity's built-in avoidance is turned off for the robot because it doesn't match how real serving robots behave; only path planning is used. Arrival rates, eating times and the robot/staff split follow the earlier SimPy model, so the two can be compared. Group sizes, drink bar visits, restroom visits and chair pulling are placeholders, and they are exactly what I want to count in a real restaurant. The full Unity code and settings (43 files), the design doc and every request to Claude Code are in the post."""
lc_ja="""作りについての補足です。ロボットは直径0.5 m の円柱で、前のセンサーの決まりで動きます(0.6 m 以内に何かあれば止まる、1秒空いたら動き出す、10秒たったら別の道を探す、60秒で動けなくなった扱い)。Unity の自動でよける機能は、実際の配膳ロボットのよけ方と違うので、ロボットには使っていません。道を探す部分だけを使っています。来店のペース、食事の時間、ロボットとスタッフの運び分けは前の SimPy の試算とそろえていて、2つを並べられるようにしています。1組の人数、ドリンクバーやトイレに行く回数、椅子の引き方は仮の値で、ここが実際のお店で数えたいところです。Unity のコードと設定43本の全文、設計書、Claude Code への依頼文はすべて記事に載せています。"""
posts={"x":x,"facebook":fb,"linkedin":li_en+"\n\n—\n\n"+li_ja,"linkedin_comment":lc_en+"\n\n—\n\n"+lc_ja}
d={"meta":{"day":357,"date":"2026-10-21","title":title,"url":url,"meta_description":meta,"tags":tags},"posts":posts}
O='/mnt/user-data/outputs/LOG357/'
json.dump(d,open(O+'LOG357_sns_posts.json','w',encoding='utf-8'),ensure_ascii=False,indent=2)
txt=f"LOG357 SNS投稿\nタイトル: {title}\nURL: {url}\n\n■ X\n{x}\n\n■ Facebook\n{fb}\n\n■ LinkedIn\n{posts['linkedin']}\n\n■ LinkedIn(補足コメント)\n{posts['linkedin_comment']}\n"
open(O+'LOG357_sns_posts.txt','w',encoding='utf-8').write(txt)
print(len(x.replace(url,'x'*23)), len(meta))