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