using System.Collections.Generic; using UnityEditor; using UnityEngine; namespace ServiceSim.EditorTools { /// /// 人とロボットが「いま何をしているか」を、頭の上に1行で出す(Scene ビューだけ)。 /// /// 見るだけのもの。読むのは、人とロボットのスクリプトがすでに持っている /// 「今の仕事」の値だけで、表示のための値は持たない。 /// 当たり判定も処理の順番も変えない(Editor 側で描くだけ)。 /// /// 動画に映す表示(Game ビュー)は、段階4で設計書 8.6 と一緒に作る。 /// [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/全員"; const string PlaceKey = "ServiceSim.ShowPlaceNames"; const string MenuPlaces = "ServiceSim/Show Place Names(場所の名前)"; /// 出す相手(B5-3) enum Who { RobotOnly = 0, RobotAndStaff = 1, All = 2 } // ---- 見た目(B5)---- /// 字の大きさ(点)。Scene ビューの既定より大きくする const int FontSize = 13; /// 帯どうしが重ならないように、上下にずらす幅(画面の点) const float RowHeightPx = 17f; /// この距離(画面の点)より近い帯は、重なっているとみなして上へずらす const float OverlapPx = 90f; // ---- 場所の名前(吹き出しと見分けがつくように、小さい字・薄い背景・枠つき)---- /// 場所の名前の字の大きさ(点)。吹き出しより小さくする const int PlaceFontSize = 10; static readonly Color PlaceFill = new Color(1.00f, 1.00f, 1.00f, 0.55f); static readonly Color PlaceLine = new Color(0.20f, 0.22f, 0.28f, 0.90f); static readonly Color PlaceText = new Color(0.10f, 0.11f, 0.14f); static GUIStyle _placeStyle; static GUIStyle _style; static StoreBuilder _store; static readonly List _tables = new List(); /// この回に描いた帯の画面上の場所(重なりを避けるのに使う) static readonly List _drawn = new List(); // 誰の字幕かを分ける、帯の左端の色(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 ShowPlaces { get { return EditorPrefs.GetBool(PlaceKey, true); } set { EditorPrefs.SetBool(PlaceKey, value); } } 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); } } /// 出す相手。初期値はロボットとスタッフ(B5-3) 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; } /// /// 計測の実行中は字幕を出さない(B11)。 /// 字幕は Scene ビューで毎コマ描くので、35卓ぶんあると倍速に効く。 /// メニューの設定(EditorPrefs)は変えない。回し終われば自動で元に戻る。 /// static bool MeasuringNow { get { if (!EditorApplication.isPlaying) return false; ExperimentRunner r = UnityEngine.Object.FindObjectOfType(); if (r == null || !r.Running) return false; // 見る用の回(Run One > 見る用)は消さない。 // ExperimentPlan で「回している間も出す」にしてあるときも消さない if (r.viewMode) return false; return r.plan == null || !r.plan.showLabelsWhileRunning; } } [MenuItem(MenuPlaces, false, 104)] static void TogglePlaces() { ShowPlaces = !ShowPlaces; SceneView.RepaintAll(); } [MenuItem(MenuPlaces, true)] static bool TogglePlacesValidate() { Menu.SetChecked(MenuPlaces, ShowPlaces); return true; } [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) { // キッチンのモニターの吹き出しは、**測る回でも出す**(B11 とは別。2026-10-23)。 // 1つだけなので、倍速にほとんど効かない bool monitorOnly = MeasuringNow; if (!Show && !ShowPlaces && !monitorOnly) return; EnsureStyle(); RefreshStore(); _drawn.Clear(); if (monitorOnly) { Handles.BeginGUI(); DrawKitchenMonitor(view); Handles.EndGUI(); return; } Handles.BeginGUI(); if (ShowPlaces) DrawPlaceNames(); // 先に描いて、吹き出しがその上に重ならないようにする if (Show) { DrawRobots(); if (ShowWho != Who.RobotOnly) DrawStaff(); if (ShowWho == Who.All) DrawGuests(); if (ShowSeated) DrawSeated(); // 椅子を引いている卓は、相手の設定に関わらず出す } // 画面に貼り付けるパネルは、いちばん最後(ほかの札の重なり避けには入れない) DrawKitchenMonitor(view); Handles.EndGUI(); } static void EnsureStyle() { if (_placeStyle == null) { // 場所の名前は、吹き出しより小さい字・濃い文字色(背景は薄い色+枠) _placeStyle = new GUIStyle(EditorStyles.label); _placeStyle.alignment = TextAnchor.MiddleCenter; _placeStyle.fontStyle = FontStyle.Normal; _placeStyle.fontSize = PlaceFontSize; _placeStyle.normal.background = null; _placeStyle.padding = new RectOffset(5, 5, 1, 1); _placeStyle.normal.textColor = PlaceText; } 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(); _tables.Clear(); if (_store == null) return; IList t = _store.Tables; for (int i = 0; i < t.Count; i++) _tables.Add(t[i]); } /// /// 頭の上に1行の字幕を出す(B5)。 /// 濃い半透明の帯を敷いて文字は白にし、誰の字幕かは帯の左端の色で分ける。 /// 近くの帯どうしは上へずらして重ならないようにする。 /// 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); } /// /// すでに描いた帯と重なるなら、重ならなくなるまで上へずらす。 /// 近い帯どうしだけを見る(画面の離れたところは見ない)。 /// 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; } // ---------------- キッチンのモニター(画面に貼り付けるパネル)---------------- const string MonitorXKey = "ServiceSim.MonitorX"; const string MonitorYKey = "ServiceSim.MonitorY"; const string MonitorOpenKey = "ServiceSim.MonitorOpen"; /// パネルの見出し(掴んで動かす・押してたたむ) const string MonitorTitle = "キッチンのモニター"; /// 画面のふちからの余白(点) const float MonitorMarginPx = 8f; static GUIStyle _monitorStyle; static GUIStyle _monitorTitleStyle; static bool _monitorDragging; static bool _monitorMoved; static Vector2 _monitorGrab; /// パネルを開いているか(たたんでいないか) static bool MonitorOpen { get { return EditorPrefs.GetBool(MonitorOpenKey, true); } set { EditorPrefs.SetBool(MonitorOpenKey, value); } } /// パネルの画面の上の位置(点。はじめは左上) static Vector2 MonitorPos { get { return new Vector2(EditorPrefs.GetFloat(MonitorXKey, MonitorMarginPx), EditorPrefs.GetFloat(MonitorYKey, MonitorMarginPx)); } set { EditorPrefs.SetFloat(MonitorXKey, value.x); EditorPrefs.SetFloat(MonitorYKey, value.y); } } /// /// キッチンのモニター(2026-10-23)。Scene ビューの画面に貼り付けるパネル1つ。 /// カメラを回してもズームしても、画面の上の位置は変わらない。 /// 見出しを掴むと動かせ、押すとたためる。位置とたたみ方は EditorPrefs に覚える。 /// 測る回でも出す(頭の上の字幕を切る B11 とは別)。 /// 切り替えは ExperimentPlan.showKitchenMonitor(KitchenMonitor.show に入っている)。 /// static void DrawKitchenMonitor(SceneView view) { KitchenMonitor monitor = Object.FindObjectOfType(); if (monitor == null || !monitor.show) return; // 回していないあいだは見本(場所と大きさを確かめられるように) string text = monitor.Text; if (string.IsNullOrEmpty(text)) { if (EditorApplication.isPlaying) return; text = KitchenMonitor.SampleText(); } EnsureMonitorStyle(); GUIContent title = new GUIContent((MonitorOpen ? "▼ " : "▶ ") + MonitorTitle); GUIContent body = new GUIContent(text); Vector2 titleSize = _monitorTitleStyle.CalcSize(title); Vector2 bodySize = MonitorOpen ? _monitorStyle.CalcSize(body) : Vector2.zero; float w = Mathf.Max(titleSize.x, bodySize.x) + 10f; // 左端の色の帯のぶん float headerH = Mathf.Max(titleSize.y, 16f); float h = headerH + (MonitorOpen ? bodySize.y + 4f : 0f); Vector2 pos = MonitorPos; Rect panel = new Rect(pos.x, pos.y, w, h); Rect header = new Rect(pos.x, pos.y, w, headerH); MonitorMouse(view, panel, header, w, h); pos = MonitorPos; // 動かしたぶんを読み直す panel = new Rect(pos.x, pos.y, w, h); header = new Rect(pos.x, pos.y, w, headerH); EditorGUI.DrawRect(panel, new Color(0.05f, 0.06f, 0.09f, 0.88f)); EditorGUI.DrawRect(new Rect(panel.x + 4f, header.yMax - 1f, panel.width - 4f, 1f), new Color(1f, 1f, 1f, 0.25f)); GUI.Label(new Rect(header.x + 5f, header.y, header.width - 5f, header.height), title, _monitorTitleStyle); if (MonitorOpen) GUI.Label(new Rect(panel.x + 5f, header.yMax + 2f, panel.width - 6f, bodySize.y), body, _monitorStyle); } /// /// パネルの上のマウスを受け取る。 /// ・見出しを掴んで動かす。押すとたためる・開ける /// ・**パネルの上のクリックとドラッグは、ここで受け取って先へ渡さない** /// (後ろにある卓・人・ロボットが選ばれたり動いたりしないようにする) /// ・画面の外に出たら中へ戻す(パネルがつかめなくならないように) /// static void MonitorMouse(SceneView view, Rect panel, Rect header, float w, float h) { Event e = Event.current; if (e == null) return; bool onPanel = panel.Contains(e.mousePosition); if (e.type == EventType.MouseDown && onPanel) { if (e.button == 0 && header.Contains(e.mousePosition)) { _monitorDragging = true; _monitorMoved = false; _monitorGrab = e.mousePosition - new Vector2(header.x, header.y); } e.Use(); // 後ろの物を選ばせない return; } if (e.type == EventType.MouseDrag) { if (_monitorDragging) { _monitorMoved = true; MonitorPos = ClampToView(view, e.mousePosition - _monitorGrab, w, h); e.Use(); if (view != null) view.Repaint(); return; } if (onPanel) { e.Use(); return; } // 後ろの物を動かさせない } if (e.type == EventType.MouseUp) { if (_monitorDragging) { _monitorDragging = false; if (!_monitorMoved) MonitorOpen = !MonitorOpen; // 動かさずに押したら、たたむ・開ける MonitorPos = ClampToView(view, MonitorPos, w, h); e.Use(); if (view != null) view.Repaint(); return; } if (onPanel) { e.Use(); return; } } // 画面の大きさが変わったときのために、描くたびに中へ戻す MonitorPos = ClampToView(view, MonitorPos, w, h); } /// パネルを画面の中に収める static Vector2 ClampToView(SceneView view, Vector2 pos, float w, float h) { if (view == null) return pos; float vw = view.position.width; float vh = view.position.height; float maxX = Mathf.Max(MonitorMarginPx, vw - w - MonitorMarginPx); float maxY = Mathf.Max(MonitorMarginPx, vh - h - MonitorMarginPx); return new Vector2(Mathf.Clamp(pos.x, MonitorMarginPx, maxX), Mathf.Clamp(pos.y, MonitorMarginPx, maxY)); } static void EnsureMonitorStyle() { if (_monitorStyle == null) { // 字の大きさは、場所の名前の札と同じ。文は左寄せの複数行 _monitorStyle = new GUIStyle(EditorStyles.label); _monitorStyle.alignment = TextAnchor.UpperLeft; _monitorStyle.fontStyle = FontStyle.Normal; _monitorStyle.fontSize = PlaceFontSize; _monitorStyle.normal.background = null; _monitorStyle.padding = new RectOffset(0, 0, 0, 0); _monitorStyle.normal.textColor = Color.white; _monitorStyle.wordWrap = false; } if (_monitorTitleStyle != null) return; _monitorTitleStyle = new GUIStyle(_monitorStyle); _monitorTitleStyle.fontStyle = FontStyle.Bold; _monitorTitleStyle.alignment = TextAnchor.MiddleLeft; } // ---------------- 場所の名前 ---------------- /// /// 場所の名前を、その場所の床の真ん中に出す。 /// /// 吹き出し(スタッフの「待機」など)と同じ作り方 /// (Handles.BeginGUI の中の GUI.Label)なので、 /// ・カメラの向きに関係なく画面に正対する /// ・ズームしても字の大きさが変わらない /// 見分けがつくように、字は小さく・背景は薄く・枠をつける。 /// static void DrawPlaceNames() { if (_store == null || _store.Plan == null) return; List names = new List(); List at = new List(); CollectPlaceNames(_store, names, at); for (int i = 0; i < names.Count; i++) PlaceLabel(at[i], names[i]); } /// /// 出す場所と短い名前(13個)。1つの場所に1つだけ。 /// 同じ種類が2つ以上ある待機の点は、まとめて1つにする。 /// public static void CollectPlaceNames(StoreBuilder store, List names, List at) { names.Clear(); at.Clear(); StorePlan plan = store != null ? store.Plan : null; if (plan == null) return; 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[] text = { "入口", "風除室", "待合", "レジ", "レジの内側", "厨房", "受け渡し口", "返却口", "パントリー", "ドリンクバー", "トイレ" }; for (int k = 0; k < kinds.Length; k++) { bool found = false, hasRect = false; PlanRect area = new PlanRect(0f, 0f, 0f, 0f); Vector3 point = Vector3.zero; 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 = hasRect ? new PlanRect(Mathf.Min(area.xMin, p.rect.xMin), Mathf.Min(area.zMin, p.rect.zMin), Mathf.Max(area.xMax, p.rect.xMax), Mathf.Max(area.zMax, p.rect.zMax)) : p.rect; hasRect = true; } if (!found) continue; names.Add(text[k]); at.Add(hasRect ? area.Center : point); // 場所の床の真ん中 } AddStandbyName(store.RobotStandbyPoints, "ロボット待機", names, at); AddStandbyName(store.StaffStandbyPoints, "スタッフ待機", names, at); } static void AddStandbyName(IList points, string name, List names, List at) { if (points == null || points.Count == 0) return; Vector3 sum = Vector3.zero; for (int i = 0; i < points.Count; i++) sum += points[i]; names.Add(points.Count > 1 ? name + "×" + points.Count : name); at.Add(sum / points.Count); } /// 場所の名前1つ(小さい字・薄い背景・枠つき) static void PlaceLabel(Vector3 world, string text) { Camera cam = Camera.current; if (cam == null) return; 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; Vector3 sp = HandleUtility.WorldToGUIPointWithDepth(world); if (sp.z < 0f) return; Vector2 size = _placeStyle.CalcSize(new GUIContent(text)); float w = size.x; float h = Mathf.Max(size.y, PlaceFontSize + 6f); Rect rect = new Rect(sp.x - w * 0.5f, sp.y - h * 0.5f, w, h); rect = AvoidOverlap(rect); _drawn.Add(rect); // 薄い背景 EditorGUI.DrawRect(rect, PlaceFill); // 枠(1 点の線を4本) EditorGUI.DrawRect(new Rect(rect.x, rect.y, rect.width, 1f), PlaceLine); EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 1f, rect.width, 1f), PlaceLine); EditorGUI.DrawRect(new Rect(rect.x, rect.y, 1f, rect.height), PlaceLine); EditorGUI.DrawRect(new Rect(rect.xMax - 1f, rect.y, 1f, rect.height), PlaceLine); GUI.Label(rect, text, _placeStyle); } // ---------------- ロボット ---------------- static void DrawRobots() { IReadOnlyList 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); } } /// /// ロボットの字幕(B5-5 で短くした言い方)。 /// 止まっている台は帯を黄、動けない台は帯を赤にする(B5-1)。 /// 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; // v5 3章:スタッフが歩いて来るのを待っている(受け渡し口へは移さない) return "動けない・人を待つ " + Mathf.FloorToInt(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 ""; } /// 1走りで2卓以上回るときは、卓の数だけ添える(短く) static string TableListText(ServingRobotController r) { if (r.TableIds.Count <= 1) return ""; return "(" + r.TableIds.Count + "卓)"; } /// /// 止まった原因だけを短く。センサーがすでに持っている値から作る /// (「客」「スタッフ」「椅子」「ロボット」「道がない」)。 /// 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(); // 同じ文で近くに立っている人はまとめる(例「スタッフ×2 待機」) List anchors = new List(); List texts = new List(); List counts = new List(); 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); } } /// この距離の中にいる同じ文の吹き出しは、1つにまとめる(m) const float MergeM = 1.0f; /// /// スタッフの字幕は仕事の名前だけにする(場所は記録に出るので書かない)。 /// 会計・案内・料理・片付け・呼び出し・待機 /// static string StaffText(StaffAgent a) { if (a.CurrentJob == null) return "待機"; return StaffAgent.JobKindName(a.CurrentJob.kind); } // ---------------- 客 ---------------- static void DrawGuests() { GuestAgent[] all = Object.FindObjectsOfType(); // 待合の客は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(); int outside = gm != null ? Mathf.Max(0, gm.WaitingGroups - waitingBodies) : 0; Label(waitingAnchor, 2.2f, "待合 " + waitingBodies + "人・外で待つ " + outside + "組", TagWaiting); } } /// /// 人がロボットをよけている最中かを、字幕の前に付ける。 /// 「ロボット待ち」「よける」「回り道」。不注意の人は印を付ける。 /// static string YieldText(GameObject go) { PersonRobotYield y = go != null ? go.GetComponent() : 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 + "] " : ""; } /// /// 客の字幕(最低限)。出すのは3つのときだけ。 /// 「待つ」「よける」「回り道」… ロボットをよけている /// 「ロボットの相手」… ロボットが止まった相手になった /// 「あきらめ」… 行き先に着けずあきらめた /// static string GuestShortText(GuestAgent g) { PersonRobotYield y = g.GetComponent(); 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 ""; } /// その人が、いま止まっているロボットの相手になっているか static bool IsRobotStopCause(Transform who) { IReadOnlyList 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(); 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); } } /// /// その卓の椅子が、いまロボットの止まった相手になっているか(B5-3)。 /// 「全員」のときも、座っている客と椅子は、これに当てはまるときだけ出す。 /// static bool BlamedByRobot(Vector3 at) { IReadOnlyList 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; } } }