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; }
}
}