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