using System.Collections.Generic; using System.Text; using UnityEngine; namespace ServiceSim { /// /// キッチンのモニター(2026-10-23 エイイチと決めた。設計書 v0 の 0 章)。 /// ロボット・スタッフのいまの仕事と止まりの理由、店の様子を1つにまとめて出す。 /// /// 出し方は **Scene ビューの画面に貼り付けるパネル1つ**(描くのは ServiceSimLabels)。 /// カメラを回してもズームしても、画面の上の位置は変わらない。 /// 屋根の上に板を置く作り・店の上に浮かべる吹き出しは、どちらもやめた /// (読めなくなる/通路やほかの札に重なるため)。 /// /// この script は **文を作るだけ**で、見た目は持たない。 /// ・当たり判定・NavMesh・ロボットのセンサー・人がよける処理には入らない /// ・言葉と数字は、記録に使っているものだけ(LABEL_RULES.md の表) /// ・文を作り直すのは店の中の 0.5 秒ごと(毎コマ作らない) /// ・頭の上の字幕は測る回では切る(B11)が、このパネル1つだけは出す /// public class KitchenMonitor : MonoBehaviour { [Header("参照(Setup が入れる)")] public StoreBuilder store; public ServiceOps ops; public GroupManager groups; public PeopleSpawner people; [Header("出し方")] [Tooltip("出すか。ExperimentPlan.showKitchenMonitor が入れる")] public bool show = true; [Tooltip("文を作り直す間隔(店の中の秒)。毎コマ作らない")] public float refreshSec = 0.5f; [Header("いまの様子(読むだけ)")] [SerializeField] int _lines; float _nextAtS; string _text = ""; readonly StringBuilder _sb = new StringBuilder(); /// この部品を置く親の名前 public const string RootName = "KitchenMonitor"; /// いま出す文(ServiceSimLabels が読む)。空なら出さない public string Text { get { return _text; } } /// 行の数(読むだけ) public int Lines { get { return _lines; } } void Update() { if (!show) { _text = ""; _lines = 0; return; } if (ops == null) return; // 回していないあいだは見本を出す(描く側が決める) float now = ops.NowS; if (now < _nextAtS) return; _nextAtS = now + Mathf.Max(0.05f, refreshSec); _text = Compose(); _lines = CountLines(_text); } /// /// 店を建てたあと、まだ回していないときに出す見本(ServiceSimLabels が読む)。 /// 場所と大きさを確かめられるようにするため /// public static string SampleText() { return "ロボット×2\n" + " 1号機 配膳 卓19 / 一時停止:人が立ち止まっていた\n" + " 2号機 戻る\n" + "スタッフ×4 案内 1/配膳 1/片付け 1/待機 1\n" + "ロボット待ち スタッフ 0 人 / 客 1 人(回り道 0 人)\n" + "組 0(席待ち 0・帰った 0) 皿 0\n" + "受け渡し口でロボットを待っている皿 0\n" + "あきらめた 0 / 移した 0 / 卓を空けた 0\n" + "立ち往生 0 回  ※見本(回すと本物の数字になります)"; } static int CountLines(string s) { if (string.IsNullOrEmpty(s)) return 0; int n = 1; for (int i = 0; i < s.Length; i++) if (s[i] == '\n') n++; return n; } /// /// 前の作り(屋根の上の板)で置いた子を片付ける。 /// 吹き出しに変えたので、板と文字は要らない /// public void RemoveOldBoards() { RemoveChild("Board"); RemoveChild("Backing"); } void RemoveChild(string name) { Transform t = transform.Find(name); if (t == null) return; if (Application.isPlaying) Destroy(t.gameObject); else DestroyImmediate(t.gameObject); } // ---------------- 文を作る ---------------- string Compose() { _sb.Length = 0; // ---- ロボット(台ごとに仕事と、止まっているときは理由)---- IList rs = ops.Robots; _sb.Append("ロボット×").Append(rs.Count).Append('\n'); for (int i = 0; i < rs.Count; i++) { ServingRobotController r = rs[i]; if (r == null) continue; _sb.Append(" ").Append(r.robotId).Append("号機 ").Append(RobotJob(r)); string why = StopText(r); if (why.Length > 0) _sb.Append(" / ").Append(why); _sb.Append('\n'); } // ---- スタッフ(仕事ごとの人数)---- AppendStaff(); // ---- ロボット待ち(スタッフと客の人数)---- int waitStaff, waitGuest, detours; CountYielding(out waitStaff, out waitGuest, out detours); _sb.Append("ロボット待ち スタッフ ").Append(waitStaff) .Append(" 人 / 客 ").Append(waitGuest).Append(" 人") .Append("(回り道 ").Append(detours).Append(" 人)").Append('\n'); // ---- 店の様子 ---- if (groups != null) _sb.Append("組 ").Append(groups.GroupsArrived) .Append("(席待ち ").Append(groups.WaitingGroups) .Append("・帰った ").Append(groups.GroupsDone).Append(")"); _sb.Append(" 皿 ").Append(ops.DishesWaiting).Append('\n'); // ---- 受け渡し口でロボットを待っている皿(サイトの試算 v2.7)---- // 料理ができてから robot_first_wait_s のあいだは、ロボットだけが運ぶ。 // そのあいだ、まだ誰も持っていない皿の数(記録と同じ数え方) _sb.Append("受け渡し口でロボットを待っている皿 ") .Append(ops.DishesWaitingForRobot).Append('\n'); _sb.Append("あきらめた ").Append(ops.gaveUpCount) .Append(" / 移した ").Append(ops.warpedCount) .Append(" / 卓を空けた ").Append(ops.tableFreedCount).Append('\n'); _sb.Append("立ち往生 ").Append(ops.rescueCalls).Append(" 回"); return _sb.ToString(); } /// ロボットの仕事(LABEL_RULES の言葉) static string RobotJob(ServingRobotController r) { switch (r.CurrentPhase) { case ServingRobotController.Phase.ToTable: return "配膳 卓" + r.TableId.ToString("00"); case ServingRobotController.Phase.Dwell: return "配膳 卓" + r.TableId.ToString("00"); case ServingRobotController.Phase.ToPass: return "戻る"; case ServingRobotController.Phase.StuckWait: return "立ち往生"; default: return "待機"; } } /// 止まっているときの理由(動いていれば空) static string StopText(ServingRobotController r) { if (r.CurrentPhase == ServingRobotController.Phase.StuckWait) return ""; // 仕事の側に出ている if (r.spec != null && r.SpeedMps > r.spec.stoppedSpeedEpsilon) return ""; string why = r.StopCauseNow; if (why.Length == 0) return ""; return "一時停止:" + why; } void AppendStaff() { IReadOnlyList staff = StaffAgent.All; _sb.Append("スタッフ×").Append(staff.Count); Dictionary byJob = new Dictionary(); for (int i = 0; i < staff.Count; i++) { if (staff[i] == null) continue; string job = MonitorJobName(staff[i]); int n; byJob.TryGetValue(job, out n); byJob[job] = n + 1; } bool first = true; foreach (KeyValuePair kv in byJob) { _sb.Append(first ? " " : "/").Append(kv.Key).Append(' ').Append(kv.Value); first = false; } _sb.Append('\n'); } /// /// スタッフの仕事の名前。記録(staff_time_s)の言葉は過去の記録と比べるため変えないので、 /// ここだけ LABEL_RULES の表の言葉に直して出す(料理 → 配膳)。 /// static string MonitorJobName(StaffAgent a) { if (a.CurrentJob == null) return "待機"; switch (a.CurrentJob.kind) { case StaffJobKind.Guide: return "案内"; case StaffJobKind.Serve: return "配膳"; case StaffJobKind.Call: return "呼び出し"; case StaffJobKind.Pay: return "会計"; case StaffJobKind.Clear: return "片付け"; case StaffJobKind.Rescue: return "立ち往生"; } return "待機"; } /// /// いまロボットをよけている人を数える。 /// ロボット待ち … 入口で待つ・通路の中で下がる(止まっている) /// 回り道 … 別の通路へまわっている(止まっていない) /// void CountYielding(out int waitStaff, out int waitGuest, out int detours) { waitStaff = 0; waitGuest = 0; detours = 0; IReadOnlyList all = PersonBody.All; for (int i = 0; i < all.Count; i++) { PersonBody b = all[i]; if (b == null || b.kind == PersonKind.Seated) continue; PersonRobotYield y = b.GetComponent(); if (y == null || !y.Intervening) continue; if (y.CurrentMode == PersonRobotYield.Mode.Detour) { detours++; continue; } if (b.kind == PersonKind.Staff) waitStaff++; else waitGuest++; } } } }