using System.Collections.Generic; using System.IO; using UnityEngine; namespace ServiceSim { /// /// 条件を回して記録する。1回の Play の中で、条件を順に通せる(段階3の Run All)。 /// 数字と場所は ExperimentPlan の .asset に置く。回し方だけメニューが決める。 /// /// この script は実験だけを扱う。店の運営は ServiceOps、人は GroupManager に任せる。 /// public class ExperimentRunner : MonoBehaviour { /// 時間の進め方 public enum TimeMode { RealTime, FastTimeScale, FixedStep } /// 回す条件1つ [System.Serializable] public class Job { public int aisleWidthIndex; public CrowdLevel crowd; public int robots; public int seed; public string folder; public string conditionId; /// /// 卓の停止点を通路の中心からずらす距離(m)。メニューが決める(C1)。 /// applyDeliveryOffset が false のときは、.asset の値をそのまま使う。 /// true のときも、使うのはこの回だけ。回し終わりに .asset の値へ戻すので、 /// .asset は書き換わらない(run_info.json の delivery_offset_used_m に実際の値が出る)。 /// public bool applyDeliveryOffset; public float deliveryOffset; // ---- サイトの条件で回す(v5 3章)---- /// /// サイトの条件(unity_conditions)で回すか。 /// true なら、店の回し方の数字は serving-robot-params.json から読み、 /// 始め方もサイトと同じ「空の店から peak_min 分だけ来客し、全員が帰るまで」にする。 /// public bool siteRun; /// unity_conditions の何番めか(siteRun のとき) public int conditionIndex = -1; /// 試算の stores の名前(fam_p1 / fam_p5)。報告とフォルダ名に使う public string storeKey = ""; } [Header("参照(Setup が入れる)")] public ExperimentPlan plan; public ServiceSpec service; [Tooltip("サイトの試算の値(v5 3章)。サイトの条件で回すときに読む")] public SimParams simParams; public StoreBuilder store; public NavMeshBaker baker; public PeopleSpawner people; public ServiceOps ops; public GroupManager groups; public JamWatcher jams; public RobotSpec robotSpec; public GameObject robotPrefab; public Transform robotsRoot; [Header("回し方(メニューが決める)")] public bool runOnPlay; /// /// 見る用の回か。字幕と描画をこの回だけ出し、時間は1倍速で進める。 /// 設定ファイル(ExperimentPlan)は書き換えない。計測の比べには使わない。 /// public bool viewMode; public TimeMode timeMode = TimeMode.FixedStep; public string runLabel = "v2"; public float captureDeltaTimeOverride; [Header("いまの様子(読むだけ)")] [SerializeField] int _jobIndex; [SerializeField] int _jobCount; [SerializeField] float _simMinutes; [SerializeField] float _wallSeconds; [SerializeField] int _runsDone; [SerializeField] bool _running; // Play に入るとき、Unity はいちど script を読み直す(Domain Reload)。 // そのときに消えないように、回す条件は .unity に書き出して持たせる。 // メニューが入れた条件が Start() まで残らないと「回す条件がありません」で止まる。 [SerializeField] List _queue = new List(); readonly TripLogger _logger = new TripLogger(); readonly List _robots = new List(); readonly TripLogger.RunInfo _info = new TripLogger.RunInfo(); readonly List _walkerSamples = new List(); /// 計測が始まってからこの回数走っても記録が0なら、取りこぼしとみて止める const int TripRecordAlarmRuns = 3; Job _job; // 停止点は、メニューが決めた値をこの回だけ使う(C1)。 // .asset を書き換えたままにしないよう、もとの値を覚えて回し終わりに戻す。 float _savedDeliveryOffset; bool _deliveryOffsetSaved; // サイトの試算の値も、この回だけ入れて回し終わりに戻す(v5 3章) readonly SiteValues _siteValues = new SiteValues(); SimParams.Condition _condition; /// 来客を受け付け終わる時刻(店の中, 秒)。siteRun のときだけ使う float _arrivalsEndS; /// 全員が帰るのを待つ上限(店の中, 秒)。これを超えたら打ち切る float _hardEndS; /// 全員が帰って終わったか(打ち切りでないか) bool _everyoneLeft; float _endS, _measureStartS, _wallStart, _lastLog, _timeOffset; int _tripSeq; int _runsFinished; // ロボットが走り終わった回数(慣らしも含む) int _runsAfterMeasure; // 計測が始まってから走り終わった回数 bool _stoppedEarly; bool _jobRunning; /// この条件ぶんを書き出したか(途中で止めたときに二重に書かないため。v5 3章) bool _outputsWritten; public bool Running { get { return _running; } } /// 回す条件を1つだけ入れる(段階2のやり方) public void SetSingleJob(Job job) { _queue.Clear(); _queue.Add(job); } /// 回す条件をまとめて入れる(段階3の Run All) public void SetQueue(IList jobs) { _queue.Clear(); if (jobs != null) for (int i = 0; i < jobs.Count; i++) _queue.Add(jobs[i]); } void Start() { if (!runOnPlay) return; if (!Resolve()) return; if (_queue.Count == 0) { Debug.LogError("[ServiceSim] 回す条件がありません"); return; } Application.runInBackground = true; ApplyTimeMode(); // 見る用の回は、描画を止めない(字幕も ServiceSimLabels が出す) if (plan.disableRenderingWhileRunning && !viewMode) DisableCameras(); _jobIndex = 0; _jobCount = _queue.Count; _runsDone = 0; _wallStart = Time.realtimeSinceStartup; _running = true; StartJob(_queue[0]); } bool Resolve() { if (store == null) store = FindObjectOfType(); if (baker == null) baker = FindObjectOfType(); if (people == null) people = FindObjectOfType(); if (ops == null) ops = FindObjectOfType(); if (groups == null) groups = FindObjectOfType(); if (jams == null) jams = FindObjectOfType(); if (robotsRoot == null && store != null && store.transform.parent != null) robotsRoot = store.transform.parent.Find("Robots"); if (plan == null || service == null || store == null || baker == null || people == null || ops == null || groups == null || jams == null || robotPrefab == null || robotsRoot == null || store.layout == null) { Debug.LogError("[ServiceSim] ExperimentRunner の参照が足りません。ServiceSim/Setup を実行してください"); return false; } return true; } void ApplyTimeMode() { Time.captureDeltaTime = 0f; switch (timeMode) { case TimeMode.RealTime: Time.timeScale = 1f; break; case TimeMode.FastTimeScale: Time.timeScale = plan.trialTimeScaleFast; break; default: Time.timeScale = 1f; Time.captureDeltaTime = captureDeltaTimeOverride > 0f ? captureDeltaTimeOverride : plan.measureCaptureDeltaTime; break; } } void DisableCameras() { Camera[] cams = FindObjectsOfType(); for (int i = 0; i < cams.Length; i++) cams[i].enabled = false; } // ---------------- 1条件ぶん ---------------- void StartJob(Job job) { _job = job; _logger.Clear(); _info.frameDeltas.Clear(); _info.robotSteps.Clear(); _info.peopleSteps.Clear(); _info.chairPullsByReason.Clear(); _info.chairPulls = 0; _walkerSamples.Clear(); _tripSeq = 0; _runsFinished = 0; _runsAfterMeasure = 0; _stoppedEarly = false; _everyoneLeft = false; _outputsWritten = false; _condition = null; StoreLayout layout = store.layout; layout.aisleWidthIndex = job.aisleWidthIndex; // 停止点は条件の1つ(C1)。メニューが決めた値を、組み立てる前に入れる。 // .asset のもとの値は覚えておき、ぜんぶ終わったら戻す(.asset は書き換えない)。 if (!_deliveryOffsetSaved) { _savedDeliveryOffset = layout.deliveryOffsetFromAisleCenter; _deliveryOffsetSaved = true; } layout.deliveryOffsetFromAisleCenter = job.applyDeliveryOffset ? job.deliveryOffset : _savedDeliveryOffset; job.conditionId = ExperimentPlan.ConditionId(layout.AisleWidth, job.crowd, job.robots); // サイトの条件で回すとき(v5 3章)。 // 建てる前に値を入れる(卓の数・来客のペース・時間の決まりが組み立てと来客に効く)。 // 1つでも読めない値があれば、回さずに止める(Unity の .asset で埋めない) if (job.siteRun && !ApplySiteValues(job)) { AbortRun(); return; } store.Build(); // 建てた卓の数が、試算の stores.tables と違ったら回さない(v5 3章)。 // 店の形(JSON)が合わないときは Unity で埋めずに止める if (job.siteRun && !TablesMatchSite(job)) { AbortRun(); return; } baker.Bake(); // 見る用の回は、床の塗り分けを Build Store と同じに作り直す。 // 計測の回では作らない(見た目だけのものなので、速さを落とさない) if (viewMode) store.PaintFloor(baker, people.spec != null ? people.spec.personRadius : 0.2f); ops.spec = service; ops.robotSpec = robotSpec; ops.store = store; ops.Setup(); people.service = service; people.ops = ops; people.Spawn(job.seed); groups.spec = service; groups.people = people.spec; groups.ops = ops; groups.store = store; groups.spawner = people; // 人の動きに合わせて椅子を引く(B6) groups.bodyRoot = transform; jams.spec = people.spec; jams.ops = ops; jams.store = store; jams.Setup(); SpawnRobots(job.robots); // 始め方(v5 3章)。 // サイトの条件 … 空の店から、peak_min 分だけ来客し、全員が帰るまで回す // (max_extra_min で打ち切り)。慣らしは無し // それ以外 … これまでどおり、8割埋まった店+慣らし 15 分 if (job.siteRun) { groups.seedWarmupGroups = false; _arrivalsEndS = service.peakMinutes * 60f; groups.arrivalsEndS = _arrivalsEndS; _hardEndS = _arrivalsEndS + service.maxExtraMin * 60f; _measureStartS = 0f; _endS = _hardEndS; } else { groups.seedWarmupGroups = true; groups.arrivalsEndS = 0f; _arrivalsEndS = 0f; _hardEndS = 0f; _measureStartS = plan.warmupMinutes * 60f; _endS = (plan.warmupMinutes + plan.measureMinutes) * 60f; } groups.Setup(job.crowd, job.seed, people.Targets); ops.SetStaffCount(people.StaffSpawned); _timeOffset = Time.time; _lastLog = 0f; _jobRunning = true; Debug.Log("[ServiceSim] 回し始め " + (_jobIndex + 1) + "/" + _jobCount + ":" + job.conditionId + " 種" + job.seed + (job.siteRun ? "(サイトの条件。空の店から来客 " + service.peakMinutes + " 分 + 全員が帰るまで(上限 " + service.maxExtraMin + " 分)" + "・スタッフ " + people.StaffSpawned + " 人・ロボット " + job.robots + " 台)" : "(慣らし " + plan.warmupMinutes + " 分 + 計測 " + plan.measureMinutes + " 分)")); } /// /// 合わない値があって回せないとき。書き出さずに Play を抜ける(v5 3章)。 /// .asset に入れた値は、OnDisable と同じようにもとへ戻す /// void AbortRun() { _running = false; _jobRunning = false; RestoreDeliveryOffset(); RestoreSiteValues(); Time.captureDeltaTime = 0f; Time.timeScale = 1f; Debug.LogError("[ServiceSim] 回しませんでした。Console の上の行を見てください"); #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #endif } /// /// 建てた卓の数が、試算の stores.{店}.tables と合っているか(v5 3章)。 /// 合わなければ、どちらかが古い。Unity で埋めずに止める /// bool TablesMatchSite(Job job) { JsonStoreLayout js = store.layout as JsonStoreLayout; if (js == null || simParams == null) return true; // ops.Setup() はこのあとなので、建てた数は店の形(JSON)から数えたものを見る int want = Mathf.RoundToInt(simParams.StoreValue(job.storeKey, "tables", -1f)); int got = js.TableCount; if (want < 0 || want == got) return true; Debug.LogError("[ServiceSim] 卓の数が合いません(回しません):試算 stores." + job.storeKey + ".tables = " + want + " / 建てた店 = " + got + "\n どちらかが古いので、サイトから写し直してください"); return false; } /// /// サイトの試算の値を、この回だけ ServiceSpec / PeopleSpec / RobotSpec に入れる(v5 3章)。 /// 版の番号(店の形の meta.generation ↔ 試算の meta.layout_generation)が合わなければ回さない。 /// 読めない値が1つでもあれば回さない(Unity の .asset で埋めない)。 /// bool ApplySiteValues(Job job) { if (simParams == null) { Debug.LogError("[ServiceSim] SimParams が入っていません(ServiceSim/Setup を実行してください)"); return false; } JsonStoreLayout js = store.layout as JsonStoreLayout; if (js == null) { Debug.LogError("[ServiceSim] サイトの条件で回すには、JSON から建てた店(JsonStoreLayout)が要ります。" + "いまの店: " + store.layout); return false; } string storeKey = string.IsNullOrEmpty(job.storeKey) ? js.ParamsStoreKey : job.storeKey; job.storeKey = storeKey; if (!simParams.Load(js.ReadGeneration())) { string why = ""; for (int i = 0; i < simParams.Problems.Count; i++) why += "\n - " + simParams.Problems[i]; Debug.LogError("[ServiceSim] 試算の値を読めません(回しません)" + why); return false; } _condition = job.conditionIndex >= 0 ? simParams.ConditionAt(job.conditionIndex) : null; if (_condition == null) { Debug.LogError("[ServiceSim] 条件 " + job.conditionIndex + " が unity_conditions にありません"); return false; } if (_condition.store != storeKey) { Debug.LogError("[ServiceSim] 条件の店(" + _condition.store + ")と、建てる店(" + storeKey + ")が違います"); return false; } if (!_siteValues.Apply(simParams, storeKey, _condition, service, people.spec, robotSpec)) { string why = ""; for (int i = 0; i < _siteValues.Problems.Count; i++) why += "\n - " + _siteValues.Problems[i]; Debug.LogError("[ServiceSim] 試算の値が足りません(回しません)" + why); return false; } job.robots = _condition.robots; job.conditionId = _condition.Id; Debug.Log("[ServiceSim] " + _siteValues.Report()); return true; } void SpawnRobots(int count) { ops.ClearRobots(); for (int i = robotsRoot.childCount - 1; i >= 0; i--) Destroy(robotsRoot.GetChild(i).gameObject); _robots.Clear(); // 置き始めは、1台に1つの待機場所へ(v5)。 // 受け渡し口の前に重ねて置くと、はじめからふさぎ合う Vector3 pass = store.PassPoint; IList standby = store.RobotStandbyPoints; for (int i = 0; i < count; i++) { GameObject go = Instantiate(robotPrefab, robotsRoot); go.name = "ServingRobot" + (i + 1).ToString("00"); ServingRobotController c = go.GetComponent(); if (c == null) { Destroy(go); continue; } c.robotId = i + 1; Vector3 start = standby.Count > 0 ? standby[i % standby.Count] : pass + new Vector3(0f, 0f, -0.8f * i); c.PlaceAt(start, Quaternion.LookRotation(Vector3.back, Vector3.up)); c.TripCompleted += OnRunDone; _robots.Add(c); ops.RegisterRobot(c); } } void Update() { if (!_running || !_jobRunning) return; float dt = Time.deltaTime; _wallSeconds = Time.realtimeSinceStartup - _wallStart; _info.frameDeltas.Add(dt); float step = 0f; for (int i = 0; i < _robots.Count; i++) if (_robots[i] != null) step = Mathf.Max(step, _robots[i].LastStepM); _info.robotSteps.Add(step); // 人の1コマの移動量も記録する(B10)。通路を歩く人のうちいちばん大きいもの float peopleStep = 0f; IReadOnlyList bodies = PersonBody.All; for (int i = 0; i < bodies.Count; i++) { PersonBody b = bodies[i]; if (b == null || !b.walksInAisle) continue; if (b.LastStepM > peopleStep) peopleStep = b.LastStepM; } _info.peopleSteps.Add(peopleStep); ops.Advance(dt); groups.Tick(dt); jams.Tick(dt); _simMinutes = ops.NowS / 60f; if (ops.NowS >= _measureStartS) _walkerSamples.Add(groups.WalkingBodies + people.StaffSpawned); if (_wallSeconds - _lastLog >= 60f) { _lastLog = _wallSeconds; // 「走り」はロボットが走り終わった回数、「記録」は trips.csv に書いた行数。 // 走りがあるのに記録が増えないときは、記録の取りこぼしなのでその場で止める Debug.Log("[ServiceSim] とちゅう " + (_jobIndex + 1) + "/" + _jobCount + ":" + _simMinutes.ToString("0.0") + " / " + (_job.siteRun ? "来客 " + (_arrivalsEndS / 60f).ToString("0") + " 分・上限 " + (_hardEndS / 60f).ToString("0") + " 分" : (_endS / 60f).ToString("0") + " 分") + "、" + "組 " + groups.GroupsArrived + "(待ち " + groups.WaitingGroups + "・帰った " + groups.GroupsDone + ")、" + "走り " + _runsFinished + " 回・記録 " + _logger.RowCount + " 行、" + "椅子 " + ops.chairPulls + " 回、" + "あきらめ " + (ops.gaveUpCount + ops.warpedCount + ops.tableFreedCount) + " 回、" + "立ち往生 " + ops.rescueCalls + " 回、" + "実時間 " + (_wallSeconds / 60f).ToString("0.0") + " 分"); CheckTripsAreRecorded(); } if (ShouldFinish()) FinishJob(); } /// /// 1条件を終える時。 /// サイトの条件(v5 3章)… 来客を受け付け終わって、全員が帰ったら終わり。 /// 帰らないまま max_extra_min を過ぎたら打ち切る /// それ以外 … これまでどおり、慣らし+計測の長さで終わり /// bool ShouldFinish() { if (!_job.siteRun) return ops.NowS >= _endS; if (ops.NowS >= _arrivalsEndS && !groups.AnyGroupInStore) { _everyoneLeft = true; return true; } return ops.NowS >= _hardEndS; } /// /// 走りがあるのに trips.csv の行が増えないときは、記録の取りこぼし。 /// 気づかないまま何時間も回さないように、その場で止めて知らせる。 /// 慣らしの間に始まった走りは数えないので、計測が始まってから見る。 /// void CheckTripsAreRecorded() { if (ops.NowS < _measureStartS) return; if (_runsAfterMeasure <= 0) return; if (_logger.RowCount > 0) return; // 計測が始まってから走り終わった回数がこれを超えても記録が0なら、おかしい if (_runsAfterMeasure < TripRecordAlarmRuns) return; Debug.LogError("[ServiceSim] 走りが " + _runsAfterMeasure + " 回あるのに trips.csv の行が 0 です。記録を取りこぼしています。" + "\n 止めました。ExperimentRunner.OnRunDone と " + "ServingRobotController.RunStartS を確かめてください" + "\n 店の中 " + (ops.NowS / 60f).ToString("0.0") + " 分 / 計測の始まり " + (_measureStartS / 60f).ToString("0.0") + " 分"); StopEarly(); } /// 途中で止める(書き出しはする) void StopEarly() { _stoppedEarly = true; FinishJob(); AllDone(); } void OnRunDone(ServingRobotController r) { _runsFinished++; if (ops.NowS >= _measureStartS) _runsAfterMeasure++; // 走り始めた時刻はロボットが持っている(ServingRobotController.RunStartS)。 // ここで覚えておくと、走り終わってすぐ次の仕事が入ったとき、 // 「空いている」状態を一度も見ないまま時刻が古いままになり、全部の走りを取りこぼす。 float startS = r.RunStartS >= 0f ? r.RunStartS : ops.NowS - r.TotalS; if (startS < _measureStartS) return; IList batch = ops.LastBatchOf(r); _tripSeq++; TripLogger.Row row = new TripLogger.Row(); row.conditionId = _job.conditionId; row.aisleWidthM = store.layout.AisleWidth; row.crowd = ExperimentPlan.CrowdName(_job.crowd); row.robots = _job.robots; row.seed = _job.seed; row.robotId = r.robotId; row.tripId = _tripSeq; row.tableId = r.TableIds.Count > 0 ? r.TableIds[0] : 0; row.distanceM = r.DistanceM; row.totalS = r.TotalS; row.stoppedS = r.StoppedS; row.nStops = r.StopCount; row.stopDurationsS = ToArray(r.StopDurationsS); row.stuck = r.Stuck; row.timeout = r.TimedOut; row.waitAisleS = r.WaitAisleS; row.maxDecelMps2 = r.MaxDecelMps2; row.minClearanceM = r.MinClearanceM; row.metRobot = r.MetRobot; int[] ids = new int[r.TableIds.Count]; for (int i = 0; i < ids.Length; i++) ids[i] = r.TableIds[i]; row.tableIds = ids; row.dishes = batch.Count; float[] ready = new float[batch.Count], arrived = new float[batch.Count]; int undelivered = 0; for (int i = 0; i < batch.Count; i++) { ready[i] = batch[i].readyS; arrived[i] = batch[i].arrivedS; if (batch[i].arrivedS < 0f) undelivered++; } row.dishReadyS = ready; row.dishArrivedS = arrived; row.undelivered = undelivered; // ---- 往復ごとの測り(v5 3章・設計書2章の4つ)---- // 実際に走れた速さは、止まっていた分を除いた時間で出す(止まり込みの平均は別物) float movingS = r.TotalS - r.StoppedS; row.speedMps = movingS > 0.01f ? r.DistanceM / movingS : 0f; row.stuckCount = r.StuckCount; row.stuckWaitS = r.StuckWaitS; row.viaNarrow = r.LegWasNarrow; SplitStopsByAisle(r, ref row); _logger.Add(row); AddStopRows(r, _tripSeq); } void AddStopRows(ServingRobotController robot, int tripId) { float normal = NormalWidthM(); IReadOnlyList events = robot.StopEvents; for (int i = 0; i < events.Count; i++) { ServingRobotController.StopEvent e = events[i]; TripLogger.StopRow s = new TripLogger.StopRow(); s.conditionId = _job.conditionId; s.seed = _job.seed; s.robotId = robot.robotId; s.tripId = tripId; s.tS = e.startTimeS - _timeOffset; s.x = e.position.x; s.z = e.position.z; s.aisle = AisleNameAt(e.position); s.inNarrow = InNarrowAisle(e.position, normal); s.cause = TripLogger.CauseLabel(e.cause); s.causeDetail = e.causeDetail; s.pathBlocked = e.pathBlocked; s.durationS = e.durationS; s.resolved = TripLogger.ResolveLabel(e.resolve); s.clearanceLeftCm = e.clearanceLeftM * 100f; s.clearanceRightCm = e.clearanceRightM * 100f; s.minGapCm = Mathf.Min(s.clearanceLeftCm, s.clearanceRightCm); // 何で動けなかったかの内訳(B7) s.blockedS = e.blockedS; s.noPathS = e.noPathS; s.yieldS = e.yieldS; s.clearWaitS = e.clearWaitS; s.replans = e.replans; // 道を引いたときの中身(調べるためだけ) s.solveFrom = Xz(e.solveFrom); s.solveTo = Xz(e.solveTo); s.solveStatus = string.IsNullOrEmpty(e.solveStatus) ? "なし" : e.solveStatus; s.solveCorners = e.solveCorners; s.offMeshM = e.offMeshM; _logger.AddStop(s); } } /// /// 止まりを、狭い通路の中と外に分けて数える(v5 3章。⑦の読み分け)。 /// 「狭い」はサイトの決まりのまま、幅が meta.settings.normal_width(1.0 m)未満の通路。 /// ③の通路は 1.2 m なので、ぜんぶ「外」になる。 /// void SplitStopsByAisle(ServingRobotController robot, ref TripLogger.Row row) { float normal = NormalWidthM(); IReadOnlyList events = robot.StopEvents; for (int i = 0; i < events.Count; i++) { if (InNarrowAisle(events[i].position, normal)) { row.stopsInNarrow++; row.stoppedInNarrowS += events[i].durationS; } else { row.stopsOutNarrow++; row.stoppedOutNarrowS += events[i].durationS; } } } /// サイトの「ふだん走らせる道の幅」(m)。JSON に無ければ 0(=ぜんぶ「外」) float NormalWidthM() { JsonStoreLayout js = store != null ? store.layout as JsonStoreLayout : null; return js != null ? js.NormalWidth : 0f; } bool InNarrowAisle(Vector3 p, float normalWidth) { if (normalWidth <= 0f) return false; IList aisles = store.Aisles; for (int i = 0; i < aisles.Count; i++) { if (aisles[i].width >= normalWidth) continue; Bounds b = aisles[i].zone; if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue; return true; } return false; } /// 点を "x;z" の1つの欄にする(stops.csv の solve_from / solve_to) static string Xz(Vector3 p) { return p.x.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture) + ";" + p.z.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture); } string AisleNameAt(Vector3 p) { string wide = ""; IList aisles = store.Aisles; for (int i = 0; i < aisles.Count; i++) { Bounds b = aisles[i].zone; if (p.x < b.min.x || p.x > b.max.x || p.z < b.min.z || p.z > b.max.z) continue; if (aisles[i].narrow) return aisles[i].name; if (wide.Length == 0) wide = aisles[i].name; } return wide; } static float[] ToArray(IReadOnlyList list) { if (list == null || list.Count == 0) return new float[0]; float[] a = new float[list.Count]; for (int i = 0; i < a.Length; i++) a[i] = list[i]; return a; } // ---------------- 書き出して次へ ---------------- void FinishJob() { _jobRunning = false; AddGroupRows(); WriteOutputs(_job.folder, _stoppedEarly); _jobIndex++; if (_jobIndex < _queue.Count) { StartJob(_queue[_jobIndex]); return; } AllDone(); } /// /// 1条件ぶんの記録を書き出す(v5 3章で切り出した)。 /// 途中で Play を止めたときも、ここまでの分を同じ形で書き出せるようにするため、 /// 置き場所と「途中で止めたか」を渡す形にした。 /// 書き出しの中で、オブジェクトを探したり消したりしないこと /// (Play を抜ける最中にも呼ばれるため)。 /// void WriteOutputs(string dir, bool stoppedEarly) { _outputsWritten = true; try { Directory.CreateDirectory(dir); TripLogger.SimLike sim = new TripLogger.SimLike(); sim.dishWaitMin.AddRange(ops.dishWaitMin); sim.seatWaitMin.AddRange(ops.seatWaitMin); sim.turnMin.AddRange(ops.turnMin); sim.coldMin = service.coldMin; sim.robotDishes = ops.robotDishes; sim.staffDishes = ops.staffDishes; sim.requeuedDishes = ops.requeuedDishes; sim.groups = groups.GroupsDone; sim.jamCount = jams.JamCount; sim.jamTotalS = jams.TotalJamSeconds; sim.robotBlockedJams = jams.RobotBlockedJamCount; // B9 sim.chairPulls = ops.chairPulls; // B6 sim.robotContacts = ops.Contacts.Count; // 人がロボットをよける sim.robotYieldWaits = ops.robotYieldWaits; sim.robotYieldWaitSeconds = ops.robotYieldWaitSeconds; sim.robotYieldDetours = ops.robotYieldDetours; sim.registerCrowdMax = ops.registerCrowdMax; // レジの前の人数 sim.registerCrowdMean = ops.RegisterCrowdMean; sim.registerPayCount = ops.registerPayCount; sim.registerPayNotAtBackCount = ops.registerPayNotAtBackCount; sim.registerPayOffSpotCount = ops.registerPayOffSpotCount; sim.registerPayMeanOffsetM = ops.RegisterPayMeanOffsetM; sim.registerPayMaxOffsetM = ops.registerPayMaxOffsetM; sim.registerBackIntruderCount = ops.registerBackIntruderCount; sim.registerBackIntruderSeconds = ops.registerBackIntruderSeconds; sim.staffTime.Clear(); foreach (KeyValuePair kv in ops.StaffTime) sim.staffTime.Add(kv); sim.dishParts.Clear(); sim.dishParts.AddRange(ops.DishPartsList); sim.jobWaitMean.Clear(); foreach (KeyValuePair kv in ops.JobWaitSum) { int n; ops.JobWaitCount.TryGetValue(kv.Key, out n); sim.jobWaitMean.Add(new KeyValuePair(kv.Key, n > 0 ? kv.Value / n : 0f)); } sim.drinkBarQueued = ops.drinkBarQueued; // B4 sim.drinkBarGaveUp = ops.drinkBarGaveUp; sim.drinkBarWaitMin.Clear(); sim.drinkBarWaitMin.AddRange(ops.drinkBarWaitMin); sim.gaveUp = ops.gaveUpCount; // B1 sim.warped = ops.warpedCount; sim.tableFreed = ops.tableFreedCount; sim.walkersMean = MeanInt(_walkerSamples); sim.measuredMinutes = _job.siteRun ? ops.NowS / 60f : plan.measureMinutes; // ---- サイトと同じ名前で出す値(v5 3章・meta.metrics)---- sim.entryWaitMin.AddRange(ops.entryWaitMin); sim.fullWaitMin.AddRange(ops.fullWaitMin); sim.blockedWaitMin.AddRange(ops.blockedWaitMin); sim.guideWaitMin.AddRange(ops.guideWaitMin); sim.staffWaitMin.AddRange(ops.staffWaitMin); sim.walkKmPerStaff = ops.WalkKmPerStaff; sim.staffUtil = StaffUtil(); sim.robotUtil = ops.RobotUtil; sim.rescues = ops.rescueCalls; sim.rescuesDone = ops.rescueDone; sim.rescueWaitS = ops.rescueWaitSeconds; sim.pauses = _logger.TotalStopCount(); sim.siteRun = _job.siteRun; sim.siteConditionId = _job.conditionId; sim.siteResult.Clear(); if (_condition != null) foreach (KeyValuePair kv in _condition.siteResult) sim.siteResult.Add(kv); sim.arrivalsMinutes = _job.siteRun ? _arrivalsEndS / 60f : 0f; sim.totalMinutes = ops.NowS / 60f; sim.everyoneLeft = _everyoneLeft; _logger.SetSimLike(sim); _logger.WriteTripsCsv(Path.Combine(dir, "trips.csv")); _logger.WriteStopsCsv(Path.Combine(dir, "stops.csv")); _logger.WriteGroupsCsv(Path.Combine(dir, "groups.csv")); TripLogger.WriteJamsCsv(Path.Combine(dir, "people_jams.csv"), _job.conditionId, _job.seed, jams.Rows); TripLogger.WriteGiveUpsCsv(Path.Combine(dir, "give_ups.csv"), _job.conditionId, _job.seed, ops.GiveUps); TripLogger.WriteRequeuesCsv(Path.Combine(dir, "dishes.csv"), _job.conditionId, _job.seed, ops.Requeues); TripLogger.WriteContactsCsv(Path.Combine(dir, "contacts.csv"), _job.conditionId, _job.seed, ops.Contacts); _logger.WriteSummaryJson(Path.Combine(dir, "summary.json"), plan.runName); _info.runLabel = runLabel; _info.conditionId = _job.conditionId; _info.timeMode = timeMode.ToString(); _info.tripsRequested = 0; _info.tripsDone = _logger.RowCount; _info.robots = _job.robots; _info.seed = _job.seed; _info.aisleWidthM = store.layout.AisleWidth; _info.deliveryOffsetM = store.layout.deliveryOffsetFromAisleCenter; _info.deliveryOffsetUsedM = store.layout.DeliveryOffset(); _info.crowd = ExperimentPlan.CrowdName(_job.crowd); _info.simSeconds = ops.NowS; _info.wallSeconds = _wallSeconds; _info.timeScale = timeMode == TimeMode.FastTimeScale ? plan.trialTimeScaleFast : 1f; _info.captureDeltaTime = Time.captureDeltaTime; // 椅子を引いた回数(B6) _info.chairPulls = ops.chairPulls; _info.chairPullsByReason.Clear(); foreach (KeyValuePair kv in ops.ChairPullsByReason) _info.chairPullsByReason.Add(kv); _info.stoppedEarly = stoppedEarly; _info.storeKind = store.layout.name; TripLogger.WriteRunInfoJson(Path.Combine(dir, "run_info.json"), _info); TripLogger.WriteConditionsJson(Path.Combine(dir, "conditions.json"), plan.runName, store.layout, robotSpec, people.spec, plan, plan.BuildConditions(store.layout)); _runsDone++; Debug.Log("[ServiceSim] 書き出し " + (_jobIndex + 1) + "/" + _jobCount + ":" + _job.conditionId + " 種" + _job.seed + "\n 走り " + _runsFinished + " 回 / 記録 " + _logger.RowCount + " 行 / 止まり " + _logger.StopCount + " 回 / 組 " + _logger.GroupRowCount + "\n 椅子を引いた " + ops.chairPulls + " 回 / 人どうしの詰まり " + jams.JamCount + " 回(うちロボットが近くで止まっていた " + jams.RobotBlockedJamCount + " 回)" + "\n 会計 " + ops.registerPayCount + " 回(店員側にいなかった " + ops.registerPayNotAtBackCount + " 回・立つ点から離れた " + ops.registerPayOffSpotCount + " 回・いちばん離れた " + ops.registerPayMaxOffsetM.ToString("0.00") + " m・会計でないのに店員側にいた " + ops.registerBackIntruderCount + " 回)/ レジの前の人数 最大 " + ops.registerCrowdMax + "・平均 " + ops.RegisterCrowdMean.ToString("0.0") + "\n 人がロボットを待った " + ops.robotYieldWaits + " 回・" + ops.robotYieldWaitSeconds.ToString("0") + " 秒 / 回り道 " + ops.robotYieldDetours + " 回 / 近づきすぎ " + ops.Contacts.Count + " 回" + "\n 戻りの線を使えなかった " + ReturnLineFallbacks() + " 回" + "(使えないときは受け渡し口の前へ戻る)" + "\n 立ち往生で呼んだ " + ops.rescueCalls + " 回(動かした " + ops.rescueDone + " 回・待った " + ops.rescueWaitSeconds.ToString("0") + " 秒)" + "\n 入店待ち " + Mean(ops.entryWaitMin).ToString("0.00") + " 分(満席 " + Mean(ops.fullWaitMin).ToString("0.00") + "・片付け待ち " + Mean(ops.blockedWaitMin).ToString("0.00") + "・案内待ち " + Mean(ops.guideWaitMin).ToString("0.00") + ")/ スタッフの歩き " + ops.WalkKmPerStaff.ToString("0.00") + " km・手がふさがり " + (StaffUtil() * 100f).ToString("0") + "%・ロボット " + (ops.RobotUtil * 100f).ToString("0") + "%" + "\n あきらめた " + ops.gaveUpCount + " / 移した " + ops.warpedCount + " / 卓を空けた " + ops.tableFreedCount + " / 戻した皿 " + ops.requeuedDishes + "(届いた " + ops.robotDishes + ")" + "\n " + TripLogger.FrameStatsText(_info) + "\n " + dir); } catch (System.Exception e) { Debug.LogError("[ServiceSim] 書き出しに失敗: " + dir + "\n" + e); } } void AddGroupRows() { IList gs = groups.Groups; for (int i = 0; i < gs.Count; i++) { GroupManager.Group g = gs[i]; TripLogger.GroupRow r = new TripLogger.GroupRow(); r.conditionId = _job.conditionId; r.seed = _job.seed; r.groupId = g.id; r.size = g.size; r.tableId = g.table != null ? g.table.tableId : 0; r.tableSeats = g.table != null ? g.table.seatCount : 0; // 2人席/4人席 r.arriveS = g.arriveS; r.guideStartS = g.guideStartS; r.seatedS = g.seatedS; r.eatEndS = g.eatEndS; r.payStartS = g.payStartS; r.payS = g.payS; r.leftS = g.leftS; r.waitedMin = g.seatedS >= 0f && g.arriveS >= 0f ? (g.seatedS - g.arriveS) / 60f : -1f; r.warmup = g.warmup; r.state = g.state.ToString(); _logger.AddGroup(r); } } /// /// staff_util:スタッフの手がふさがっていた割合(v5 3章)。 /// ops.StaffTime が仕事ごとの秒を持っているので、「待機」以外の割合で出す。 /// float StaffUtil() { float total = 0f, idle = 0f; foreach (KeyValuePair kv in ops.StaffTime) { total += kv.Value; if (kv.Key == "待機") idle += kv.Value; } return total > 0.01f ? (total - idle) / total : 0f; } /// /// 戻りの線が使えず、今までどおり受け渡し口の前へ戻った回数(v5)。 /// 0 でなければ、線が通れる床に乗っていないか、出ていく線・ドリンクバーの列に近すぎる /// int ReturnLineFallbacks() { int n = 0; for (int i = 0; i < _robots.Count; i++) if (_robots[i] != null) n += _robots[i].ReturnLineFallbacks; return n; } static float Mean(List v) { if (v == null || v.Count == 0) return 0f; float s = 0f; for (int i = 0; i < v.Count; i++) s += v[i]; return s / v.Count; } static float MeanInt(List v) { if (v == null || v.Count == 0) return 0f; long s = 0; for (int i = 0; i < v.Count; i++) s += v[i]; return (float)s / v.Count; } /// /// メニューが決めた停止点を、.asset のもとの値へ戻す(C1)。 /// 書き出しが終わったあとに呼ぶ。.asset を書き換えたままにしないため。 /// void RestoreDeliveryOffset() { if (!_deliveryOffsetSaved) return; _deliveryOffsetSaved = false; if (store != null && store.layout != null) store.layout.deliveryOffsetFromAisleCenter = _savedDeliveryOffset; } /// /// サイトの試算から入れた値を、.asset のもとの値へ戻す(v5 3章)。 /// 停止点(C1)と同じ扱い。.asset を書き換えたままにしないため /// void RestoreSiteValues() { _siteValues.Restore(); } void AllDone() { _running = false; RestoreDeliveryOffset(); RestoreSiteValues(); Time.captureDeltaTime = 0f; Time.timeScale = 1f; Debug.Log("[ServiceSim] ぜんぶ終わりました。" + _runsDone + " / " + _jobCount + " 条件、" + "実時間 " + (_wallSeconds / 60f).ToString("0.0") + " 分"); #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #endif } /// /// 途中で Play を止めたとき、そこまでの記録を書き出す(v5 3章)。 /// /// 回している最中に止めると、これまでは何も残らなかった。 /// 詰まった回でも give_ups.csv と summary.json を見られるように、同じ形で書き出す。 /// ・置き場所は、ふだんのフォルダ名に _stopped を付けたもの /// ・run_info.json の stopped_early が true、summary.json の /// site_run.everyone_left は false のまま /// 書き出しに失敗しても Play を抜けられるよう、まるごと try/catch で包む。 /// Play を抜ける最中なので、ここから新しくオブジェクトを探したり消したりしない。 /// void WriteOnStop() { if (!_running || !_jobRunning || _outputsWritten || _job == null) return; try { _stoppedEarly = true; _jobRunning = false; AddGroupRows(); WriteOutputs(_job.folder + "_stopped", true); Debug.LogWarning("[ServiceSim] 途中で止めました。ここまでの記録を書き出しました" + "(計測には使えません)\n 店の中 " + (ops != null ? (ops.NowS / 60f).ToString("0.0") : "?") + " 分\n " + _job.folder + "_stopped"); } catch (System.Exception e) { Debug.LogError("[ServiceSim] 途中で止めたときの書き出しに失敗しました\n" + e); } } void OnApplicationQuit() { WriteOnStop(); } void OnDisable() { // 回している最中に止めたときは、ここまでの記録を書き出す(v5 3章) WriteOnStop(); // 途中で Play を抜けたときも、停止点とサイトの値を .asset のもとの値へ戻す RestoreDeliveryOffset(); RestoreSiteValues(); Time.captureDeltaTime = 0f; if (_running) Time.timeScale = 1f; } } }