using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.AI; namespace ServiceSim.EditorTools { /// /// サイトの JSON から建てた店(JsonStoreLayout)を確かめるメニュー。 /// /// することは「建てて、正しく建ったかを確かめる」まで。回さない(段階1)。 /// 建てるところと画面の撮影は ServiceSim/Build Store をそのまま呼ぶ(作り直さない)。 /// ここで足すのは、JSON と突き合わせる確かめだけ: /// 1. 卓の数・席の数・細い通路の本数と幅(summary.json とも突き合わせる) /// 2. 止まる位置・座る位置・席に着く点・場所の点が NavMesh に乗るか /// 3. 受け渡し口の前から各卓の止まる位置までの道のりと、JSON の dist の照合(CSV) /// 4. 席に入れない卓(席に着く点へ、入口から歩いて行けるか) /// 書き出し先は ExperimentPlan.screenshotDir(Build Store と同じ)。 /// public static class ServiceSimJsonStore { const string P1Path = "Assets/ServiceSim/Store/StoreLayout_p1_efficient_corner.asset"; const string P5Path = "Assets/ServiceSim/Store/StoreLayout_p5_crammed.asset"; /// 道のりを測るときに、点から NavMesh を探す半径(m) const float SampleRadius = 0.5f; /// 点が NavMesh に乗っているとみなす距離(m) const float OnMeshRadius = 0.2f; /// 差がこれを超えた卓を報告に並べる const float BigDiffPct = 30f; [MenuItem("ServiceSim/JSON Store/③ p1_efficient_corner を建てて確かめる", false, 30)] public static void CheckP1() { Run(P1Path); } [MenuItem("ServiceSim/JSON Store/⑦ p5_crammed を建てて確かめる", false, 31)] public static void CheckP5() { Run(P5Path); } [MenuItem("ServiceSim/JSON Store/③⑦ を続けて確かめる", false, 32)] public static void CheckBoth() { if (Run(P1Path)) Run(P5Path); } const string SimParamsPath = "Assets/ServiceSim/Experiment/SimParams.asset"; /// /// サイトの試算の値(serving-robot-params.json)を読んで、中身と版の番号を確かめる(v5)。 /// 回さない。読めるかどうかと、店の形の版と合っているかだけを見る。 /// [MenuItem("ServiceSim/JSON Store/試算の値を読んで確かめる(v5)", false, 33)] public static void CheckSimParams() { SimParams sp = AssetDatabase.LoadAssetAtPath(SimParamsPath); if (sp == null) { Debug.LogError("[ServiceSim] SimParams が見つかりません: " + SimParamsPath); return; } StringBuilder sb = new StringBuilder(); sb.AppendLine("# 試算の値の読み込み(v5・回していない)"); sb.AppendLine("作った日時: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); sb.AppendLine(); // 店の形の版と突き合わせる(③⑦の両方) string[] layouts = { P1Path, P5Path }; for (int i = 0; i < layouts.Length; i++) { JsonStoreLayout layout = AssetDatabase.LoadAssetAtPath(layouts[i]); if (layout == null || layout.layoutJson == null) continue; string err; JsonValue root = JsonValue.Parse(layout.layoutJson.text, out err); string gen = root != null ? root["meta"]["generation"].S("") : "(読めない)"; bool ok = sp.Load(gen); sb.AppendLine("## " + layout.layoutFileName + " と突き合わせ → " + (ok ? "合っている" : "合っていない")); sb.AppendLine(" 店の形の版 (meta.generation): " + gen); sb.AppendLine(sp.Report()); sb.AppendLine(); } // 読む値(site_rule)を並べて、何を使うかを見えるようにする sp.Load(null); string[] keys = { "guide_s", "pay_s", "call_s", "staff_place_s", "robot_handoff_s", "clear_s", "wipe_s", "load_s", "guest_load_s", "rescue_s", "staff_speed", "cold_min", "dish_extra_m", "max_extra_min" }; sb.AppendLine("## Unity が使う値(role=site_rule)"); for (int i = 0; i < keys.Length; i++) sb.AppendLine(" " + keys[i] + " = " + F(sp.SiteRule(keys[i], float.NaN))); float[] fc = sp.SiteRuleArray("fragile_cook", null); sb.AppendLine(" fragile_cook = " + (fc == null ? "(無い)" : F(fc[0]) + " / " + F(fc[1]) + " / " + F(fc[2]))); sb.AppendLine(); sb.AppendLine("## 店ごとの値(stores)"); string[] stores = { "fam_p1", "fam_p5" }; for (int i = 0; i < stores.Length; i++) { string k = stores[i]; sb.AppendLine(" " + k + ": 卓 " + F(sp.StoreValue(k, "tables", 0f)) + " / peak_min " + F(sp.StoreValue(k, "peak_min", 0f)) + " / 来客(真ん中)" + sp.StoreMiddle(k, "arrivals", 0) + " 組毎時" + " / スタッフ(真ん中)" + sp.StoreMiddle(k, "staff", 0) + " 人" + " / eat_min " + sp.StoreTriangle(k, "eat_min", Vector3.zero) + " / call_rate " + F(sp.StoreValue(k, "call_rate", 0f)) + " / bus_trips " + F(sp.StoreValue(k, "bus_trips", 0f)) + " / fragile " + F(sp.StoreValue(k, "fragile", 0f))); } sb.AppendLine(); sb.AppendLine("## 出す数字の名前と意味(meta.metrics)"); List mk = sp.MetricKeys(); for (int i = 0; i < mk.Count; i++) sb.AppendLine(" " + mk[i] + ": " + sp.MetricNote(mk[i])); ExperimentPlan plan = AssetDatabase.LoadAssetAtPath(ServiceSimSetup.ExperimentPlanPath); string outDir = plan != null ? plan.screenshotDir : null; if (!string.IsNullOrEmpty(outDir)) { try { Directory.CreateDirectory(outDir); File.WriteAllText(Path.Combine(outDir, DateTime.Now.ToString("yyyyMMdd") + "_sim_params_check.txt"), sb.ToString()); } catch (Exception e) { Debug.LogError("[ServiceSim] 書けません: " + e.Message); } } Debug.Log("[ServiceSim] 試算の値の読み込み\n" + sb.ToString()); } static bool Run(string assetPath) { JsonStoreLayout layout = AssetDatabase.LoadAssetAtPath(assetPath); if (layout == null) { Debug.LogError("[ServiceSim] 店の形が見つかりません: " + assetPath); return false; } if (EditorSceneManager.GetActiveScene().path != ServiceSimSetup.ScenePath) { if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return false; EditorSceneManager.OpenScene(ServiceSimSetup.ScenePath); } StoreBuilder builder = UnityEngine.Object.FindObjectOfType(); NavMeshBaker baker = UnityEngine.Object.FindObjectOfType(); ExperimentPlan plan = AssetDatabase.LoadAssetAtPath(ServiceSimSetup.ExperimentPlanPath); if (builder == null || baker == null || plan == null) { Debug.LogError("[ServiceSim] StoreBuilder / NavMeshBaker / ExperimentPlan が見つかりません。" + "先に ServiceSim/Setup を実行してください"); return false; } // 建てるのは Build Store にまかせる(組み立て・ベイク・既存の確かめ・撮影) builder.layout = layout; baker.layout = layout; ServiceSimMenu.BuildStore(); builder = UnityEngine.Object.FindObjectOfType(); baker = UnityEngine.Object.FindObjectOfType(); if (builder == null || baker == null || builder.Plan == null) { Debug.LogError("[ServiceSim] 建てられませんでした。Console の [ServiceSim] の行を見てください"); return false; } if (builder.layout != layout) { Debug.LogError("[ServiceSim] Build Store のあとで店の形が " + builder.layout + " に戻っています。確かめを中止しました"); return false; } string err; JsonValue root = JsonValue.Parse(layout.layoutJson != null ? layout.layoutJson.text : "", out err); if (root == null) { Debug.LogError("[ServiceSim] JSON を読めません: " + err); return false; } string outDir = plan.screenshotDir; try { Directory.CreateDirectory(outDir); } catch (Exception e) { Debug.LogError("[ServiceSim] 書き出し先を作れません: " + outDir + "\n" + e.Message); return false; } string stem = DateTime.Now.ToString("yyyyMMdd") + "_" + layout.layoutFileName; string csvPath = Path.Combine(outDir, stem + "_table_dist.csv"); string txtPath = Path.Combine(outDir, stem + "_json_check.txt"); NavMeshQueryFilter people = baker.FilterFor(baker.PeopleAgentTypeId); // v5b: 道順はサイトの routes を使う。NavMesh は区画を足さない段階1 と同じ。 // 点から点の区間だけを NavMesh でつないで確かめる RobotSpec robotSpec = AssetDatabase.LoadAssetAtPath(ServiceSimSetup.RobotSpecPath); NavMeshQueryFilter robot = baker.FilterFor(baker.RobotAgentTypeId); _snapRadius = robotSpec != null ? robotSpec.Radius * 4f : SampleRadius; StringBuilder report = new StringBuilder(); report.AppendLine("# JSON から建てた店の確かめ(段階1・回していない)"); report.AppendLine("店の形: " + layout.name + " / " + layout.StoreKindName); report.AppendLine("作った日時: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); report.AppendLine(); report.AppendLine(layout.FrontFitReport()); report.AppendLine(); report.AppendLine(CountReport(builder, root, layout)); report.AppendLine(); report.AppendLine(OnMeshReport(builder, robot, people)); report.AppendLine(); report.AppendLine(DistReport(builder, root, robot, csvPath)); report.AppendLine(); report.AppendLine(SeatReachReport(builder, people)); try { File.WriteAllText(txtPath, report.ToString()); } catch (Exception e) { Debug.LogError("[ServiceSim] 報告を書けません: " + e.Message); } Debug.Log("[ServiceSim] JSON Store の確かめ おわり(" + layout.layoutFileName + ")\n" + report.ToString() + "\n 書き出し: " + txtPath + "\n " + csvPath); return true; } // ---------------- 1. 数 ---------------- static string CountReport(StoreBuilder builder, JsonValue root, JsonStoreLayout layout) { StringBuilder sb = new StringBuilder(); sb.AppendLine("## 1. 数"); int tables = builder.Tables.Count; int seats = 0; for (int i = 0; i < builder.Tables.Count; i++) seats += builder.Tables[i].seatCount; int jsonTables = root["tables"].Count; int jsonSeats = 0; for (int i = 0; i < jsonTables; i++) jsonSeats += root["tables"][i]["seats"].I(0); int sumTables = -1, sumSeats = -1; string err; JsonValue sum = layout.summaryJson != null ? JsonValue.Parse(layout.summaryJson.text, out err) : null; if (sum != null) for (int i = 0; i < sum.Count; i++) if (sum[i]["file"].S("") == layout.layoutFileName) { sumTables = sum[i]["tables"].I(-1); sumSeats = sum[i]["seats"].I(-1); } sb.AppendLine(" 卓: Unity " + tables + " / JSON " + jsonTables + " / summary.json " + sumTables + " " + Same(tables, jsonTables, sumTables)); sb.AppendLine(" 席: Unity " + seats + " / JSON " + jsonSeats + " / summary.json " + sumSeats + " " + Same(seats, jsonSeats, sumSeats)); int narrow = 0; StringBuilder widths = new StringBuilder(); for (int i = 0; i < builder.Aisles.Count; i++) { StoreBuilder.AisleInfo a = builder.Aisles[i]; if (!a.narrow) continue; narrow++; if (widths.Length > 0) widths.Append(" / "); widths.Append(a.name).Append(" ").Append(F(a.width)).Append(" m"); } sb.AppendLine(" 細い通路: " + narrow + " 本(" + widths + ")"); sb.AppendLine(" 組み立てた幅 (plan.aisleWidth): " + F(builder.BuiltAisleWidth) + " m"); sb.AppendLine(" 外寸: " + F(builder.OuterSize.x) + " × " + F(builder.OuterSize.y) + " m" + " / JSON outer " + F(root["outer"]["w"].F(0f)) + " × " + F(root["outer"]["d"].F(0f)) + " m"); return sb.ToString().TrimEnd(); } static string Same(int a, int b, int c) { bool ok = a == b && (c < 0 || a == c); return ok ? "一致" : "← ちがう"; } // ---------------- 2. 点が NavMesh に乗るか ---------------- static string OnMeshReport(StoreBuilder builder, NavMeshQueryFilter robot, NavMeshQueryFilter people) { StringBuilder sb = new StringBuilder(); sb.AppendLine("## 2. 点が NavMesh の上に乗るか(乗らないものだけ並べる)"); List offDelivery = new List(); List offSpot = new List(); List offSeat = new List(); List offPlace = new List(); StorePlan plan = builder.Plan; for (int i = 0; i < plan.seats.Count; i++) { PlanSeat s = plan.seats[i]; string tid = "T" + s.tableId.ToString("00"); if (!OnMesh(s.deliveryPoint, robot)) offDelivery.Add(tid + " " + P(s.deliveryPoint)); for (int k = 0; k < s.spots.Count; k++) if (!OnMesh(s.spots[k].point, people)) offSpot.Add(tid + " " + s.spots[k].name + " " + P(s.spots[k].point)); for (int k = 0; k < s.seatPoints.Count; k++) if (!OnMesh(s.seatPoints[k], people)) offSeat.Add(tid + "#" + (k + 1) + " " + P(s.seatPoints[k])); } for (int i = 0; i < plan.places.Count; i++) { PlanPlace p = plan.places[i]; if (p.solid) continue; // 部屋そのものの点は通れない箱の中 bool forRobot = p.kind == PlaceKind.FoodPass || p.kind == PlaceKind.DishReturn || p.kind == PlaceKind.RobotStandby; if (!OnMesh(p.point, forRobot ? robot : people)) offPlace.Add(p.name + " " + P(p.point) + (forRobot ? "(ロボット用)" : "(人用)")); } sb.AppendLine(" 止まる位置 (deliveryPoint, ロボット用): 乗らない " + offDelivery.Count + " / " + plan.seats.Count); Lines(sb, offDelivery); sb.AppendLine(" 席に着く点 (spots, 人用): 乗らない " + offSpot.Count); Lines(sb, offSpot); sb.AppendLine(" 座る位置 (seatPoints, 人用): 乗らない " + offSeat.Count + " ※ 座面やベンチの上の点は NavMesh に乗らないのがふつう(見た目に使う点)"); Lines(sb, offSeat, 8); sb.AppendLine(" 場所の点 (places): 乗らない " + offPlace.Count); Lines(sb, offPlace); return sb.ToString().TrimEnd(); } static void Lines(StringBuilder sb, List items, int max = 40) { for (int i = 0; i < items.Count && i < max; i++) sb.AppendLine(" - " + items[i]); if (items.Count > max) sb.AppendLine(" - ほか " + (items.Count - max) + " 件"); } static bool OnMesh(Vector3 p, NavMeshQueryFilter filter) { NavMeshHit hit; return NavMesh.SamplePosition(p, out hit, OnMeshRadius, filter); } // ---------------- 3. 道のりの照合 ---------------- /// 区間の NavMesh の道のりが、まっすぐの長さのこの倍を超えたら並べる const float SegmentDetourLimit = 1.05f; static float _snapRadius = SampleRadius; /// /// 3. 道順の照合(v5b)。 /// サイトが決めた道順(曲がり角の点)を、Unity の NavMesh でつなぎ直して確かめる。 /// ・道順の長さ(点を結んだ長さ)と JSON の dist /// ・点から点の区間が NavMesh で完全につながるか /// ・区間の NavMesh の道のりが、まっすぐの長さの 1.05 倍を超えないか /// ・use(normal / narrow)= via_narrow /// static string DistReport(StoreBuilder builder, JsonValue root, NavMeshQueryFilter robot, string csvPath) { StringBuilder sb = new StringBuilder(); sb.AppendLine("## 3. 道順の照合(受け渡し口の前 → 止まる位置。サイトの routes をたどる)"); RouteBook book = builder.Routes; if (book == null) { sb.AppendLine(" routes がありません(v5b の版の JSON を写してください)"); return sb.ToString().TrimEnd(); } sb.AppendLine(" 道順: " + book.Summary()); sb.AppendLine(" 止まる位置を面へ寄せる半径: " + F(_snapRadius) + " m"); Dictionary byId = new Dictionary(); JsonValue tables = root["tables"]; for (int i = 0; i < tables.Count; i++) { int id = NumberFromId(tables[i]["id"].S(""), i + 1); if (!byId.ContainsKey(id)) byId[id] = tables[i]; } StringBuilder csv = new StringBuilder(); csv.AppendLine("table,category,aisle,use,json_dist_m,route_len_m,unity_path_m," + "diff_m,diff_pct,via_narrow,segments,bad_segments,status"); List brokenSeg = new List(); List detourSeg = new List(); List big = new List(); List diffs = new List(); int narrowTables = 0, okTables = 0; float sumAbs = 0f, maxAbs = 0f; string maxAt = ""; StorePlan plan = builder.Plan; for (int i = 0; i < plan.seats.Count; i++) { PlanSeat s = plan.seats[i]; string tid = "T" + s.tableId.ToString("00"); JsonValue t; string category = "", aisle = ""; float jsonDist = -1f; if (byId.TryGetValue(s.tableId, out t)) { jsonDist = t["dist"].F(-1f); category = t["category"].S(""); aisle = t["aisle"].S(""); } RouteBook.Route r = book.FromPass(s.tableId); if (r == null) { brokenSeg.Add(tid + " 道順が無い"); csv.AppendLine(tid + "," + category + "," + aisle + ",,,,,,,,,,道順が無い"); continue; } bool narrow = r.IsNarrow; if (narrow) narrowTables++; float routeLen = r.PolylineLength; float unityLen = 0f; int bad = 0; bool allConnected = true; for (int k = 1; k < r.points.Count; k++) { Vector3 a = r.points[k - 1], b = r.points[k]; string st; float len = PathLen(a, b, robot, out st); float straight = Vector3.Distance(a, b); if (len < 0f) { allConnected = false; bad++; brokenSeg.Add(tid + " #" + k + " " + P(a) + "→" + P(b) + " " + st); continue; } unityLen += len; if (straight > 0.05f && len > straight * SegmentDetourLimit) { bad++; detourSeg.Add(tid + " #" + k + " まっすぐ " + F(straight) + " m → NavMesh " + F(len) + " m(" + F(len / straight) + " 倍)"); } } string status = allConnected ? "ok" : "つながらない区間あり"; if (allConnected) { okTables++; float diff = unityLen - jsonDist; float pct = jsonDist > 0.0005f ? diff / jsonDist * 100f : 0f; diffs.Add(diff); sumAbs += Mathf.Abs(diff); if (Mathf.Abs(diff) > maxAbs) { maxAbs = Mathf.Abs(diff); maxAt = tid; } if (Mathf.Abs(pct) > BigDiffPct) big.Add(tid + " JSON " + F(jsonDist) + " m / Unity " + F(unityLen) + " m(" + F(pct) + "%)"); csv.AppendLine(tid + "," + category + "," + aisle + "," + r.use + "," + F(jsonDist) + "," + F(routeLen) + "," + F(unityLen) + "," + F(diff) + "," + F(pct) + "," + (narrow ? "1" : "0") + "," + (r.points.Count - 1) + "," + bad + ",ok"); } else { csv.AppendLine(tid + "," + category + "," + aisle + "," + r.use + "," + F(jsonDist) + "," + F(routeLen) + ",," + ",," + (narrow ? "1" : "0") + "," + (r.points.Count - 1) + "," + bad + "," + status); } } try { File.WriteAllText(csvPath, csv.ToString()); } catch (Exception e) { sb.AppendLine(" CSV を書けません: " + e.Message); } sb.AppendLine(" すべての区間がつながった卓: " + okTables + " / " + plan.seats.Count); if (diffs.Count > 0) { float mean = 0f; for (int i = 0; i < diffs.Count; i++) mean += diffs[i]; mean /= diffs.Count; sb.AppendLine(" 差(Unity − JSON)の平均: " + F(mean) + " m"); sb.AppendLine(" 差の大きさの平均: " + F(sumAbs / diffs.Count) + " m / 最大: " + F(maxAbs) + " m(" + maxAt + ")"); } sb.AppendLine(" 差が " + F(BigDiffPct) + "% を超えた卓: " + big.Count); Lines(sb, big); sb.AppendLine(" via_narrow(use=narrow)の卓: " + narrowTables + " / " + plan.seats.Count); sb.AppendLine(" つながらない区間: " + brokenSeg.Count); Lines(sb, brokenSeg); sb.AppendLine(" まっすぐの " + F(SegmentDetourLimit) + " 倍を超えた区間: " + detourSeg.Count); Lines(sb, detourSeg); sb.AppendLine(" CSV: " + csvPath); return sb.ToString().TrimEnd(); } /// その絞り込みで道のりを測る。完全につながらなければ -1 static float PathLen(Vector3 from, Vector3 to, NavMeshQueryFilter filter, out string status) { NavMeshHit hitFrom, hitTo; if (!NavMesh.SamplePosition(from, out hitFrom, _snapRadius, filter)) { status = "出発点が面の上にない"; return -1f; } if (!NavMesh.SamplePosition(to, out hitTo, _snapRadius, filter)) { status = "行き先が面の上にない"; return -1f; } NavMeshPath path = new NavMeshPath(); if (!NavMesh.CalculatePath(hitFrom.position, hitTo.position, filter, path)) { status = "経路を引けない"; return -1f; } if (path.status != NavMeshPathStatus.PathComplete) { status = path.status == NavMeshPathStatus.PathPartial ? "途中まで" : "無効"; return -1f; } float len = 0f; Vector3[] c = path.corners; for (int i = 1; i < c.Length; i++) len += Vector3.Distance(c[i - 1], c[i]); status = "ok"; return len; } // ---------------- 4. 席に入れない卓 ---------------- static string SeatReachReport(StoreBuilder builder, NavMeshQueryFilter people) { StringBuilder sb = new StringBuilder(); sb.AppendLine("## 4. 席に入れない卓(入口から、席に着く点まで歩けるか)"); Vector3 from = builder.PointOf(PlaceKind.Entrance); sb.AppendLine(" 出発点 (Entrance): " + P(from)); List bad = new List(); int checkedSpots = 0; StorePlan plan = builder.Plan; for (int i = 0; i < plan.seats.Count; i++) { PlanSeat s = plan.seats[i]; if (s.spots.Count == 0) { bad.Add("T" + s.tableId.ToString("00") + " 席に着く点が無い"); continue; } bool any = false; for (int k = 0; k < s.spots.Count; k++) { checkedSpots++; string status; float len = NavMeshBaker.PathLength(from, s.spots[k].point, SampleRadius, people, out status); if (len >= 0f) { any = true; break; } } if (!any) bad.Add("T" + s.tableId.ToString("00") + " どの席に着く点へも歩けない"); } sb.AppendLine(" 確かめた点: " + checkedSpots + " / 卓 " + plan.seats.Count); sb.AppendLine(" 入れない卓: " + bad.Count); Lines(sb, bad); return sb.ToString().TrimEnd(); } // ---------------- 小さな道具 ---------------- static int NumberFromId(string id, int fallback) { if (string.IsNullOrEmpty(id)) return fallback; int n = 0; bool any = false; for (int i = 0; i < id.Length; i++) { char c = id[i]; if (c < '0' || c > '9') continue; n = n * 10 + (c - '0'); any = true; } return any ? n : fallback; } static string P(Vector3 v) { return "(" + F(v.x) + ", " + F(v.z) + ")"; } static string F(float v) { return v.ToString("0.##", CultureInfo.InvariantCulture); } } }