using System.Collections.Generic; using UnityEngine; namespace ServiceSim { /// /// すれ違い1回ぶん(passes.csv に1行。依頼 v13 の 依頼1)。 /// 同じ2台の1回は1行(2台で2行にしない)。 /// public struct PassRow { /// 「すれ違い中」に入った・終わった店の中の時刻(秒) public float startS, endS; /// すれ違った2台(号機の小さい方を a にする) public int robotA, robotB; /// いちばん近づいたときの、2台の真ん中の位置 public float x, z; /// いちばん近づいたときの体どうしの間(m)。中心どうしの距離 − 体の幅 public float minGapM; /// そのあいだで、2台のうちいちばん遅かった速さ(m/秒) public float minSpeedMps; /// /// 終わり方。 /// すれ違えた … 止まらずに通り過ぎた /// 幅が足りず今の決まりへ … 寄っても体の幅×2+間 に足りず、譲る順番の決まりに入った /// 向かい合って止まった … そのあいだに2台とも1秒以上止まった /// public string outcome; public const string OutcomePassed = "すれ違えた"; public const string OutcomeNarrow = "幅が足りず今の決まりへ"; public const string OutcomeFaceStop = "向かい合って止まった"; } /// /// ロボットどうしのすれ違いの **形の計算**(依頼 v13 の 依頼1)。 /// /// 実店舗の動画では、空の2台が狭い所で2台とも速さを落とし、止まらずに /// 体どうしの間を詰めてすれ違っていた。ここはその判定だけを置く。 /// 動かす所は ServingRobotController、数える所は PassByWatch。 /// /// 通れる床かどうかは呼ぶ側が確かめる(人とロボットで見る区画が違うため)。 /// 寄れるかどうかは **NavMesh**(体の半径 0.25 m で焼いてある)で見る。 /// 地図の free_class は「中心を置ける所」を道の幅の半分で削ったものなので、 /// 1.2 m の通路でも帯が 0.45 m しかなく、すれ違いの判定には使えない。 /// public static class RobotPassBy { /// 「向かい合っている」と見る向きの差(度)。依頼 v13 の 135 度 public const float FaceDeg = 135f; /// 向かい合って止まったと数える速さ(m/秒。依頼 v13) public const float FaceStopSpeedMps = 0.05f; /// 向かい合って止まったと数えるまでの長さ(秒。依頼 v13) public const float FaceStopMinS = 1.0f; static Vector3 Flat(Vector3 v) { v.y = 0f; return v; } /// 進む向き(止まっていても transform の向きを使う) static Vector3 Heading(ServingRobotController r) { Vector3 f = Flat(r.transform.forward); return f.sqrMagnitude < 0.000001f ? Vector3.forward : f.normalized; } /// 中心どうしの距離(m) public static float CenterDistance(ServingRobotController a, ServingRobotController b) { return Vector3.Distance(Flat(a.transform.position), Flat(b.transform.position)); } /// 体どうしの間(m)。重なっていれば 0 より小さくなる public static float BodyGap(ServingRobotController a, ServingRobotController b, RobotSpec spec) { return CenterDistance(a, b) - spec.diameter; } /// 進む向きの差が FaceDeg 以上か(=こちらへ向かってくる) public static bool HeadingsOppose(ServingRobotController a, ServingRobotController b) { return Vector3.Dot(Heading(a), Heading(b)) <= Mathf.Cos(FaceDeg * Mathf.Deg2Rad); } /// 相手が自分の前にいるか(進む向きより前) public static bool IsAhead(ServingRobotController me, ServingRobotController other) { Vector3 d = Flat(other.transform.position) - Flat(me.transform.position); if (d.sqrMagnitude < 0.000001f) return true; return Vector3.Dot(Heading(me), d.normalized) > 0f; } /// /// 「すれ違い中」に入る形か(依頼 v13)。 /// 中心どうしが passByDistanceM 以内・向きの差が 135 度以上・互いに相手が前。 /// 2台のどちらから見ても同じ答えになる(順番に頼らない) /// public static bool Starts(ServingRobotController a, ServingRobotController b, RobotSpec spec) { if (a == null || b == null || spec == null) return false; if (CenterDistance(a, b) > spec.passByDistanceM) return false; if (!HeadingsOppose(a, b)) return false; return IsAhead(a, b) && IsAhead(b, a); } /// /// 「すれ違い中」が続いているか(依頼 v13)。 /// 相手が自分の後ろ側に回るか、2台の距離が passByDistanceM を超えたら終わり。 /// 向きの差は見ない(曲がっている途中で切れないようにする) /// public static bool Continues(ServingRobotController a, ServingRobotController b, RobotSpec spec) { if (a == null || b == null || spec == null) return false; if (CenterDistance(a, b) > spec.passByDistanceM) return false; return IsAhead(a, b) && IsAhead(b, a); } /// /// すれ違いの **走り方** を続けるか。 /// /// 数の数え方(Continues)は「相手が後ろ側に回ったら終わり」だが、 /// 真横に並んだ瞬間に寄せを戻すと、体が離れないうちに相手側へ戻ってしまう。 /// そこで走り方だけは、体どうしが robotYieldClearM(0.6 m)空くまで続ける /// public static bool ContinuesMoving(ServingRobotController a, ServingRobotController b, RobotSpec spec) { if (a == null || b == null || spec == null) return false; if (CenterDistance(a, b) > spec.passByDistanceM) return false; if (IsAhead(a, b) && IsAhead(b, a)) return true; return BodyGap(a, b, spec) < spec.robotYieldClearM; } /// /// 向かい合って止まっている形か(入でも切でも数える。依頼 v13)。 /// 距離が passByDistanceM 以内・向きの差 135 度以上・2台とも 0.05 m/秒 以下 /// public static bool FacingAndStopped(ServingRobotController a, ServingRobotController b, RobotSpec spec) { if (a == null || b == null || spec == null) return false; if (CenterDistance(a, b) > spec.passByDistanceM) return false; if (!HeadingsOppose(a, b)) return false; return a.SpeedMps <= FaceStopSpeedMps && b.SpeedMps <= FaceStopSpeedMps; } /// 進む向きの右(Unity は左手系なので up × 前) public static Vector3 RightOf(Vector3 forward) { Vector3 f = Flat(forward); if (f.sqrMagnitude < 0.000001f) return Vector3.right; f.Normalize(); return new Vector3(f.z, 0f, -f.x); } } /// /// すれ違いの数を数える(依頼 v13 の 依頼1「数の定義」)。 /// /// ★ ここは **見るだけ**。動きには1つも効かない。 /// passByEnabled が切の回でも同じように数える(入の回と比べるため)。 /// 2台組ごとに1つずつ見るので、同じ2台の1回は1回(2台で2回にしない)。 /// ExperimentRunner が1コマごとに Tick を呼ぶ。 /// public class PassByWatch { /// いま続いている、2台組ぶんの様子 class Live { public int idA, idB; public bool passing; // すれ違い中 public float startS; public float minGapM; public float minSpeedMps; public Vector3 closestAt; // いちばん近づいたときの真ん中 public bool sawNarrow; // 幅が足りず今の決まりに入った public bool sawFaceStop; // そのあいだに向かい合って止まった public float faceStopS; // 2台とも止まっている秒数 public bool faceStopCounted; // 1秒を超えて1回と数えたか public string faceStopPlace; // 1回と数えた時の場所(2026-10-26) public bool faceStopOverlap; // そのあいだ体が重なったか(間が負) } readonly List _live = new List(); readonly List _rows = new List(); /// すれ違いの回数(終わった分だけ数える) public int PassCount { get; private set; } /// 減速していた秒数(ロボットごとの「すれ違い中」の合計) public float PassSlowS { get; private set; } /// 向かい合って止まった回数と、いちばん長かった秒数(ぜんぶ合わせた数) public int FaceStopCount { get; private set; } public float FaceStopMaxS { get; private set; } // ---- 2026-10-26(依頼 v13 の直し③):場所で分けて数える ---- // // 受け渡し口は点が1つしかないので、2台が同じ点へ向かうと必ず向かい合って // 両方止まる。これは「通路でふさぎ合っている」のとは別の現象なので、 // 混ぜずに分けて数える(2026-10-26 の確かめで、切の 28 件のうち // 26 件が受け渡し口だった)。 // 体が重なった(間が負)ものも別に数える(依頼 v8b の A の再発を見張る)。 /// 受け渡し口のそばで向かい合って止まった回数・最長(荷積みの順番待ち) public int FaceStopAtPassCount { get; private set; } public float FaceStopAtPassMaxS { get; private set; } /// 待機場所のそばで向かい合って止まった回数・最長 public int FaceStopAtStandbyCount { get; private set; } public float FaceStopAtStandbyMaxS { get; private set; } /// /// 通路でふさぎ合った回数・最長(受け渡し口・待機場所のそばを除いた分)。 /// **すれ違いの直しが効いたかは、この数で見る** /// public int FaceStopInAisleCount { get; private set; } public float FaceStopInAisleMaxS { get; private set; } /// そのうち、体が重なっていた(間が負)回数 public int FaceStopOverlapCount { get; private set; } /// 場所を見分けるための点(ExperimentRunner が回し始めに入れる) public void SetPlaces(Vector3 passPoint, IList standby) { _passPoint = passPoint; _standby.Clear(); if (standby != null) for (int i = 0; i < standby.Count; i++) _standby.Add(standby[i]); _hasPlaces = true; } Vector3 _passPoint; readonly List _standby = new List(); bool _hasPlaces; /// 受け渡し口・待機場所のそばと見る半径(m) const float PlaceNearM = 1.2f; /// その場所の呼び名(受け渡し口/待機場所/通路) string PlaceOf(Vector3 at) { if (!_hasPlaces) return PlaceAisle; Vector3 p = at; p.y = 0f; Vector3 q = _passPoint; q.y = 0f; if (Vector3.Distance(p, q) <= PlaceNearM) return PlaceAtPass; for (int i = 0; i < _standby.Count; i++) { Vector3 r = _standby[i]; r.y = 0f; if (Vector3.Distance(p, r) <= PlaceNearM) return PlaceAtStandby; } return PlaceAisle; } public const string PlaceAtPass = "受け渡し口"; public const string PlaceAtStandby = "待機場所"; public const string PlaceAisle = "通路"; public IReadOnlyList Rows { get { return _rows; } } public void Reset() { _live.Clear(); _rows.Clear(); PassCount = 0; PassSlowS = 0f; FaceStopCount = 0; FaceStopMaxS = 0f; FaceStopAtPassCount = 0; FaceStopAtPassMaxS = 0f; FaceStopAtStandbyCount = 0; FaceStopAtStandbyMaxS = 0f; FaceStopInAisleCount = 0; FaceStopInAisleMaxS = 0f; FaceStopOverlapCount = 0; } /// 1コマぶん見る。robots の並びのまま見るので、同じ回なら同じ順番になる public void Tick(float dt, float nowS, IList robots, RobotSpec spec) { if (spec == null || robots == null || dt <= 0f) return; // 「すれ違い中」のロボット(減速していた秒数は台ごとに足す) _slowNow.Clear(); for (int i = 0; i < robots.Count; i++) { ServingRobotController a = robots[i]; if (a == null) continue; for (int k = i + 1; k < robots.Count; k++) { ServingRobotController b = robots[k]; if (b == null) continue; TickPair(dt, nowS, a, b, spec); } } PassSlowS += dt * _slowNow.Count; } readonly List _slowNow = new List(); void TickPair(float dt, float nowS, ServingRobotController a, ServingRobotController b, RobotSpec spec) { int idA = Mathf.Min(a.robotId, b.robotId); int idB = Mathf.Max(a.robotId, b.robotId); Live L = Find(idA, idB); // ---- 向かい合って止まった(入でも切でも数える)---- if (RobotPassBy.FacingAndStopped(a, b, spec)) { if (L == null) L = Add(idA, idB); L.faceStopS += dt; // 2026-10-26(直し③):体が重なったら覚えておく float gapNow = RobotPassBy.BodyGap(a, b, spec); if (gapNow < 0f) L.faceStopOverlap = true; if (!L.faceStopCounted && L.faceStopS >= RobotPassBy.FaceStopMinS) { L.faceStopCounted = true; FaceStopCount++; // 2026-10-26(直し③):1回と数えたときの場所で振り分ける Vector3 mid = (a.transform.position + b.transform.position) * 0.5f; L.faceStopPlace = PlaceOf(mid); if (L.faceStopPlace == PlaceAtPass) FaceStopAtPassCount++; else if (L.faceStopPlace == PlaceAtStandby) FaceStopAtStandbyCount++; else FaceStopInAisleCount++; if (L.faceStopOverlap) FaceStopOverlapCount++; if (L.passing) L.sawFaceStop = true; } if (L.faceStopCounted) { if (L.faceStopS > FaceStopMaxS) FaceStopMaxS = L.faceStopS; if (L.faceStopPlace == PlaceAtPass) { if (L.faceStopS > FaceStopAtPassMaxS) FaceStopAtPassMaxS = L.faceStopS; } else if (L.faceStopPlace == PlaceAtStandby) { if (L.faceStopS > FaceStopAtStandbyMaxS) FaceStopAtStandbyMaxS = L.faceStopS; } else { if (L.faceStopS > FaceStopInAisleMaxS) FaceStopInAisleMaxS = L.faceStopS; } } } else if (L != null) { L.faceStopS = 0f; L.faceStopCounted = false; L.faceStopPlace = null; L.faceStopOverlap = false; } // ---- すれ違い中 ---- bool passing = L != null && L.passing ? RobotPassBy.Continues(a, b, spec) : RobotPassBy.Starts(a, b, spec); if (passing) { if (L == null) L = Add(idA, idB); if (!L.passing) { L.passing = true; L.startS = nowS; L.minGapM = float.MaxValue; L.minSpeedMps = float.MaxValue; L.sawNarrow = false; L.sawFaceStop = L.faceStopCounted; } float gap = RobotPassBy.BodyGap(a, b, spec); if (gap < L.minGapM) { L.minGapM = gap; L.closestAt = (a.transform.position + b.transform.position) * 0.5f; } float slower = Mathf.Min(a.SpeedMps, b.SpeedMps); if (slower < L.minSpeedMps) L.minSpeedMps = slower; if (a.PassByNarrow || b.PassByNarrow) L.sawNarrow = true; if (L.faceStopCounted) L.sawFaceStop = true; if (!_slowNow.Contains(a.robotId)) _slowNow.Add(a.robotId); if (!_slowNow.Contains(b.robotId)) _slowNow.Add(b.robotId); return; } if (L == null || !L.passing) return; // ---- すれ違いが終わった。1行にする ---- PassRow r = new PassRow(); r.startS = L.startS; r.endS = nowS; r.robotA = L.idA; r.robotB = L.idB; r.x = L.closestAt.x; r.z = L.closestAt.z; r.minGapM = L.minGapM == float.MaxValue ? 0f : L.minGapM; r.minSpeedMps = L.minSpeedMps == float.MaxValue ? 0f : L.minSpeedMps; r.outcome = L.sawFaceStop ? PassRow.OutcomeFaceStop : L.sawNarrow ? PassRow.OutcomeNarrow : PassRow.OutcomePassed; _rows.Add(r); PassCount++; L.passing = false; } Live Find(int idA, int idB) { for (int i = 0; i < _live.Count; i++) if (_live[i].idA == idA && _live[i].idB == idB) return _live[i]; return null; } Live Add(int idA, int idB) { Live L = new Live(); L.idA = idA; L.idB = idB; _live.Add(L); return L; } } }