using System.Globalization; using UnityEngine; namespace ServiceSim { /// /// 実物の動画と秒数を比べられる画面(依頼 v13 の 依頼2。2026-10-26)。 /// /// 録画はエイイチが Mac の画面録画で撮るので、Unity Recorder は入れない。 /// この script がするのは2つだけ: /// 1. 画面の左上に「店の時計」と「いまの倍率」を出す /// 2. 1倍で回す(1コマ 0.05 秒のまま、実時間に合わせて待つ) /// /// 店の時計 … 回し始めからのシミュレーションの中の経過(時:分:秒)。ServiceOps.NowS。 /// 倍率 … 直近 measureWindowSec の「シミュレーションの時間 ÷ 実時間」。 /// 決め打ちの値ではなく **測った値** を出す(コマが重いと下がる)。 /// /// 測る回に影響を出さないための決まり: /// ・show・realTimeSpeed の初期値は **どちらも切** /// ・画面なしの実行(-batchmode / -nographics)では描かない /// ・1倍で回した回は **見る専用**。ExperimentRunner が記録を書き出さない /// (出力フォルダも作らない)。段階Bなどの測る回は、1倍が入っていると /// 「押す前の確かめ」で止まる /// ・描くのは OnGUI だけで、店の回し方・ロボット・人には1つも触らない /// public class SimHud : MonoBehaviour { /// Setup が作るときの名前 public const string RootName = "SimHud"; [Header("参照(Setup が入れる)")] [Tooltip("店の時計を読む先。空なら回し始めに自分で探す")] public ServiceOps ops; [Header("出し方(録画のときだけ入れる)")] [Tooltip("画面の左上に、店の時計と倍率を出す。" + "測る回は切のままにする(初期値は切)")] public bool show = false; [Tooltip("1倍で回す。1コマ 0.05 秒のまま、実時間に合わせて待つので" + "中身の進み方は変わらない。" + "入れた回は **見る専用** で、記録を書き出さない(初期値は切)")] public bool realTimeSpeed = false; [Header("細かい決まり")] [Tooltip("倍率を測る窓(実時間の秒)。短いとちらつき、長いと反応が鈍い")] public float measureWindowSec = 2f; [Tooltip("字の大きさ(画面の高さに対する割合)。録画の見やすさで決める")] public float fontScale = 0.028f; [Tooltip("画面の左上からの余白(px)")] public Vector2 margin = new Vector2(16f, 16f); // ---- 測るための覚え(描くだけに使う。回し方には入らない)---- float _winRealS, _winSimS, _lastSimS = -1f, _lastRealS = -1f; float _rate; // 測った倍率 bool _hasRate; GUIStyle _style; Texture2D _back; /// /// 画面に描けるか(画面なしの実行では描かない)。 /// -batchmode でも -nographics なら graphicsDeviceType が Null になる /// public static bool CanDraw { get { return !Application.isBatchMode && SystemInfo.graphicsDeviceType != UnityEngine.Rendering.GraphicsDeviceType.Null; } } /// /// いま1倍で回しているか(ExperimentRunner が「見る専用」の判定に読む)。 /// 画面なしの実行では、入れてあっても1倍にしない(夜に待たせないため) /// public bool RealTimeNow { get { return realTimeSpeed && CanDraw; } } /// 測った倍率(まだ測れていなければ -1) public float MeasuredRate { get { return _hasRate ? _rate : -1f; } } void OnEnable() { _frameRealStart = Time.realtimeSinceStartup; _winRealS = 0f; _winSimS = 0f; _lastSimS = -1f; _lastRealS = -1f; _hasRate = false; } void Update() { if (ops == null) ops = FindObjectOfType(); if (ops == null) return; // 倍率を測る(店の中の時間の進み ÷ 実時間の進み)。 // // 実時間は **realtimeSinceStartup** で測る。 // Time.captureDeltaTime を入れて回していると、Unity は deltaTime も // unscaledDeltaTime もその値(0.05)に固定するので、 // そちらを使うと倍率が何倍で回していても 1.0 に見えてしまう float sim = ops.NowS; float real = Time.realtimeSinceStartup; if (_lastSimS >= 0f && _lastRealS >= 0f) { float dSim = sim - _lastSimS; float dReal = real - _lastRealS; if (dSim >= 0f && dReal > 0f) { _winSimS += dSim; _winRealS += dReal; if (_winRealS >= Mathf.Max(0.2f, measureWindowSec)) { _rate = _winSimS / _winRealS; _hasRate = true; _winSimS = 0f; _winRealS = 0f; } } } _lastSimS = sim; _lastRealS = real; } /// /// 1倍にするための待ち(依頼 v13 の 依頼2)。 /// /// Time.captureDeltaTime は触らない(1コマ 0.05 秒のまま)。 /// 1コマの実時間が 0.05 秒になるまで待つだけなので、 /// 中身の進み方(どの順で何が起きるか)は1つも変わらない。 /// LateUpdate なので、その回のコマの計算が終わったあとに待つ。 /// 画面なしの実行では待たない /// void LateUpdate() { if (!RealTimeNow) return; float step = Time.captureDeltaTime; if (step <= 0f) return; // 1コマ固定で回していないときは何もしない float used = Time.realtimeSinceStartup - _frameRealStart; float wait = step - used; if (wait > 0f && wait < 1f) { // ミリ秒まででよい(1コマ 50 ミリ秒に対して十分細かい) System.Threading.Thread.Sleep((int)(wait * 1000f)); } _frameRealStart = Time.realtimeSinceStartup; } float _frameRealStart; void Start() { _frameRealStart = Time.realtimeSinceStartup; } void OnGUI() { if (!show || !CanDraw || ops == null) return; if (_style == null) { _style = new GUIStyle(GUI.skin.label); _style.alignment = TextAnchor.UpperLeft; _style.richText = false; } _style.fontSize = Mathf.Max(11, Mathf.RoundToInt(Screen.height * fontScale)); _style.normal.textColor = Color.white; string text = Clock(ops.NowS) + " " + RateText(); Vector2 size = _style.CalcSize(new GUIContent(text)); Rect box = new Rect(margin.x, margin.y, size.x + 20f, size.y + 12f); if (_back == null) { _back = new Texture2D(1, 1); _back.SetPixel(0, 0, new Color(0f, 0f, 0f, 0.55f)); _back.Apply(); } GUI.DrawTexture(box, _back); GUI.Label(new Rect(box.x + 10f, box.y + 6f, size.x, size.y), text, _style); } /// 店の時計(時:分:秒) public static string Clock(float seconds) { if (seconds < 0f) seconds = 0f; int t = Mathf.FloorToInt(seconds); int h = t / 3600; int m = (t / 60) % 60; int s = t % 60; return h.ToString(CultureInfo.InvariantCulture) + ":" + m.ToString("00", CultureInfo.InvariantCulture) + ":" + s.ToString("00", CultureInfo.InvariantCulture); } string RateText() { if (RealTimeNow && !_hasRate) return "1.0 倍(実時間)"; if (!_hasRate) return "— 倍"; string n = _rate >= 10f ? _rate.ToString("0", CultureInfo.InvariantCulture) : _rate.ToString("0.0", CultureInfo.InvariantCulture); return n + " 倍" + (RealTimeNow ? "(実時間)" : ""); } void OnDisable() { if (_back != null) { Destroy(_back); _back = null; } } } }