using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.AI; using UnityEngine.SceneManagement; namespace ServiceSim.EditorTools { /// /// Assets/ServiceSim/ の中の .asset・prefab・シーンを作る。 /// すでにあるものは作り直さないが、マテリアルのシェーダーだけは毎回確かめて、 /// いまの描画方式に合わせ直す(合っていないとピンクになるため)。 /// Assets/ServiceSim/ の外のファイルは一切作らない・変えない。 /// 新しいシーンはビルド設定に足さない。 /// public static class ServiceSimSetup { public const string Root = "Assets/ServiceSim"; public const string ScenePath = Root + "/Scenes/ServiceSim_Family.unity"; public const string FamilyLayoutPath = Root + "/Store/StoreLayout_Family.asset"; public const string GridLayoutPath = Root + "/Store/StoreLayout_Grid.asset"; public const string RobotSpecPath = Root + "/Robot/RobotSpec.asset"; public const string PeopleSpecPath = Root + "/People/PeopleSpec.asset"; public const string ExperimentPlanPath = Root + "/Experiment/ExperimentPlan.asset"; public const string ServiceSpecPath = Root + "/Service/ServiceSpec.asset"; /// サイトの試算の値(v5)。serving-robot-params.json を読む .asset public const string SimParamsPath = Root + "/Experiment/SimParams.asset"; /// 座っている客の形(見た目だけ)の prefab public const string SeatedBodyPrefabPath = Root + "/People/SeatedGuestBody.prefab"; /// 客の黄色(立っている客・座っている客で同じ色を使う) static readonly Color GuestColor = new Color(0.98f, 0.80f, 0.13f); /// スタッフの青・待合の客の紫(見た目を作り直すときにも使う) static readonly Color StaffColor = new Color(0.09f, 0.38f, 0.90f); static readonly Color WaitingColor = new Color(0.55f, 0.30f, 0.75f); const string MatDir = Root + "/Store/Materials"; const string StorePrefabDir = Root + "/Store/Prefabs"; const string OldDir = Root + "/Store/old~"; enum WalkerKind { Staff, Guest, Waiting } static Shader _shader; static int _materialsFixed; [MenuItem("ServiceSim/Check Render Pipeline", false, 1)] public static void CheckRenderPipeline() { string how; Shader shader = ServiceSimRender.FindOpaqueShader(out how); string body = ServiceSimRender.Describe() + "使うシェーダー: " + how; if (shader == null) Debug.LogError("[ServiceSim] 描画方式に合うシェーダーが見つかりません\n" + body); else Debug.Log("[ServiceSim] 描画方式のしらべ\n" + body); } [MenuItem("ServiceSim/Use Store Layout/Family (ファミレス型)", false, 30)] public static void UseFamilyLayout() { SwitchLayout(FamilyLayoutPath); } [MenuItem("ServiceSim/Use Store Layout/Grid (格子型・比べる用)", false, 31)] public static void UseGridLayout() { SwitchLayout(GridLayoutPath); } static void SwitchLayout(string path) { StoreLayout layout = AssetDatabase.LoadAssetAtPath(path); if (layout == null) { Debug.LogError("[ServiceSim] " + path + " が読めません。先に ServiceSim/Setup を実行してください"); return; } StoreBuilder builder = UnityEngine.Object.FindObjectOfType(); NavMeshBaker baker = UnityEngine.Object.FindObjectOfType(); if (builder == null) { Debug.LogError("[ServiceSim] シーンに StoreBuilder がありません。ServiceSim_Family シーンを開いてください"); return; } builder.layout = layout; if (baker != null) baker.layout = layout; EditorUtility.SetDirty(builder); if (baker != null) EditorUtility.SetDirty(baker); EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene()); Debug.Log("[ServiceSim] 店の形を切り替えました: " + layout.name + "(" + layout.StoreKindName + ")" + "\n このあと ServiceSim/Build Store を実行してください"); } [MenuItem("ServiceSim/Setup (アセット・prefab・シーンを作る)", false, 10)] public static void Setup() { // まず描画方式を確かめる。合うシェーダーが無ければ何も作らずに止める string how; _shader = ServiceSimRender.FindOpaqueShader(out how); if (_shader == null) { Debug.LogError("[ServiceSim] この描画方式に合うシェーダーが見つかりません。" + "マテリアルを作らずに止めました。\n" + ServiceSimRender.Describe() + "探した結果: " + how); return; } _materialsFixed = 0; if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) { Debug.Log("[ServiceSim] Setup を取りやめました"); return; } EnsureFolders(); // 前の版の StoreLayout_Family.asset(格子型)は種類が変わったので、 // Unity が見ないフォルダ(old~)へ寄せておく。消さない string moved = MoveStaleFamilyLayoutAside(); Scene scene; if (File.Exists(ScenePath)) { scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single); } else { scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); EditorSceneManager.SaveScene(scene, ScenePath); scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single); } // 設定ファイル(数字の置き場所) BandStoreLayout family = LoadOrCreate(FamilyLayoutPath); LoadOrCreate(GridLayoutPath); RobotSpec robotSpec = LoadOrCreate(RobotSpecPath); PeopleSpec peopleSpec = LoadOrCreate(PeopleSpecPath); ExperimentPlan plan = LoadOrCreate(ExperimentPlanPath); ServiceSpec serviceSpec = LoadOrCreate(ServiceSpecPath); // 見た目。真上から見て、床・通路・卓・椅子・ベンチ・人・ロボットが見分けられる色にする Material matFloor = Mat("Mat_Floor", new Color(0.86f, 0.84f, 0.79f)); // 床:明るいクリーム Material matAisle = Mat("Mat_Aisle", new Color(0.52f, 0.58f, 0.67f)); // 通路:青灰色 Material matWall = Mat("Mat_Wall", new Color(0.40f, 0.43f, 0.48f)); // 壁・厨房・事務所 Material matBench = Mat("Mat_Bench", new Color(0.55f, 0.29f, 0.26f)); // ベンチ・仕切り:暗い赤茶 Material matTable = Mat("Mat_Table", new Color(0.86f, 0.62f, 0.30f)); // 卓:明るい木の色 Material matChair = Mat("Mat_Chair", new Color(0.13f, 0.37f, 0.42f)); // 椅子:濃い青緑 Material matCounter = Mat("Mat_Counter", new Color(0.20f, 0.22f, 0.26f)); // カウンター類:ほぼ黒 Material matRobot = Mat("Mat_Robot", new Color(0.96f, 0.96f, 0.97f)); // ロボット:白 Material matRobotFront = Mat("Mat_RobotFront", new Color(1.00f, 0.33f, 0.04f)); // ロボットの前:橙 Material matStaff = Mat("Mat_Staff", StaffColor); // スタッフ:青 Material matGuest = Mat("Mat_Guest", GuestColor); // 歩く客:黄 Material matWaiting = Mat("Mat_WaitingGuest", WaitingColor); // 待合の客:紫 // ベンチの見た目(当たりなし)。色と素材はここ1か所で変えられる Material matBenchSeat = Mat("Mat_BenchSeat", new Color(0.62f, 0.33f, 0.29f)); // 座面:赤茶(明るめ) Material matBenchBack = Mat("Mat_BenchBack", new Color(0.48f, 0.25f, 0.22f)); // 背もたれ:赤茶(暗め) // 床の塗り分け(見た目だけ)。家具の色が透けて見える程度にして、光と影も乗せる Material matRobotFloor = TransparentMat("Mat_RobotFloor", new Color(0.45f, 0.78f, 0.95f, 0.35f)); Material matPeopleFloor = TransparentMat("Mat_PeopleFloor", new Color(1.00f, 0.90f, 0.35f, 0.45f)); // 部品(prefab)。無ければ作る EnsureBoxPrefab(StorePrefabDir + "/Floor.prefab", "Floor", matFloor, true); EnsureBoxPrefab(StorePrefabDir + "/Wall.prefab", "Wall", matWall, true); EnsureBoxPrefab(StorePrefabDir + "/Bench.prefab", "Bench", matBench, true); EnsureBoxPrefab(StorePrefabDir + "/Table_Booth4.prefab", "Table_Booth4", matTable, true); EnsureBoxPrefab(StorePrefabDir + "/KitchenPass.prefab", "KitchenPass", matCounter, true); // 通路の色板。当たりは付けない(NavMesh に焼かれないようにする) EnsureBoxPrefab(StorePrefabDir + "/AisleMark.prefab", "AisleMark", matAisle, false); // ベンチの見た目(座面・背もたれ)。当たりは付けない // (NavMesh・前のセンサー・左右の空きの測りに効かないようにする) EnsureBoxPrefab(StorePrefabDir + "/BenchSeat.prefab", "BenchSeat", matBenchSeat, false); EnsureBoxPrefab(StorePrefabDir + "/BenchBack.prefab", "BenchBack", matBenchBack, false); // ベンチの当たりの箱。大きさ・位置・高さはベンチのまま、**見た目を持たない** EnsureColliderOnlyBoxPrefab(StorePrefabDir + "/BenchBody.prefab", "BenchBody"); // 色を変えたときに追いつくよう、すでにある prefab のマテリアルも合わせ直す SetPrefabMaterial(StorePrefabDir + "/BenchSeat.prefab", matBenchSeat); SetPrefabMaterial(StorePrefabDir + "/BenchBack.prefab", matBenchBack); EnsureChairPrefab(StorePrefabDir + "/Chair.prefab", matChair); EnsureRobotPrefab(Root + "/Robot/ServingRobot.prefab", robotSpec, matRobot, matRobotFront); EnsureWalkerPrefab(Root + "/People/Staff.prefab", "Staff", peopleSpec, matStaff, WalkerKind.Staff); EnsureWalkerPrefab(Root + "/People/Guest.prefab", "Guest", peopleSpec, matGuest, WalkerKind.Guest); EnsureWalkerPrefab(Root + "/People/WaitingGuest.prefab", "WaitingGuest", peopleSpec, matWaiting, WalkerKind.Waiting); EnsureSeatedGuestPrefab(Root + "/People/SeatedGuest.prefab", peopleSpec); EnsureSeatedBodyPrefab(SeatedBodyPrefabPath, peopleSpec, matGuest); // すでにある人の prefab の見た目を、胴+頭にそろえる(当たり判定などは触らない) EnsurePersonLook(peopleSpec); // 種類の印(Tag)。あとで映像から見分ける学習に使う。いまは付けるだけ int tagsAdded = EnsureTags(); SetPrefabTag(Root + "/Robot/ServingRobot.prefab", SimTags.Robot); SetPrefabTag(Root + "/People/Staff.prefab", SimTags.Staff); SetPrefabTag(Root + "/People/Guest.prefab", SimTags.Guest); SetPrefabTag(Root + "/People/WaitingGuest.prefab", SimTags.Guest); SetPrefabTag(SeatedBodyPrefabPath, SimTags.SeatedGuest); SetPrefabTag(StorePrefabDir + "/Chair.prefab", SimTags.Chair); SetPrefabTag(StorePrefabDir + "/BenchSeat.prefab", SimTags.Bench); SetPrefabTag(StorePrefabDir + "/BenchBack.prefab", SimTags.Bench); SetPrefabTag(StorePrefabDir + "/BenchBody.prefab", SimTags.Bench); // すでに作ってある prefab も直す。 // ・NavMeshAgent は切る(置く前に有効だと「NavMesh に近くないので作れません」の警告が出る) // ・通路の色板の当たりは外す(NavMesh に焼かれないようにする) int agentsDisabled = 0; agentsDisabled += FixPrefab(Root + "/Robot/ServingRobot.prefab", true, false) ? 1 : 0; agentsDisabled += FixPrefab(Root + "/People/Staff.prefab", true, false) ? 1 : 0; agentsDisabled += FixPrefab(Root + "/People/Guest.prefab", true, false) ? 1 : 0; agentsDisabled += FixPrefab(Root + "/People/WaitingGuest.prefab", true, false) ? 1 : 0; int collidersRemoved = FixPrefab(StorePrefabDir + "/AisleMark.prefab", false, true) ? 1 : 0; collidersRemoved += FixPrefab(StorePrefabDir + "/BenchSeat.prefab", false, true) ? 1 : 0; collidersRemoved += FixPrefab(Root + "/People/SeatedGuestBody.prefab", false, true) ? 1 : 0; // 人の種類(止まった原因を分けるのに使う)を、すでにある prefab にも入れ直す int kindsFixed = 0; kindsFixed += FixPersonKind(Root + "/People/Staff.prefab", PersonKind.Staff) ? 1 : 0; kindsFixed += FixPersonKind(Root + "/People/Guest.prefab", PersonKind.Guest) ? 1 : 0; kindsFixed += FixPersonKind(Root + "/People/WaitingGuest.prefab", PersonKind.Waiting) ? 1 : 0; kindsFixed += FixPersonKind(Root + "/People/SeatedGuest.prefab", PersonKind.Seated) ? 1 : 0; // 直したあとに読み直す(保存し直すと、前に取った参照が使えなくなるため) AssetDatabase.Refresh(); GameObject floorPrefab = Load(StorePrefabDir + "/Floor.prefab"); GameObject wallPrefab = Load(StorePrefabDir + "/Wall.prefab"); GameObject benchPrefab = Load(StorePrefabDir + "/Bench.prefab"); GameObject tablePrefab = Load(StorePrefabDir + "/Table_Booth4.prefab"); GameObject passPrefab = Load(StorePrefabDir + "/KitchenPass.prefab"); GameObject aisleMarkPrefab = Load(StorePrefabDir + "/AisleMark.prefab"); GameObject benchSeatPrefab = Load(StorePrefabDir + "/BenchSeat.prefab"); GameObject chairPrefab = Load(StorePrefabDir + "/Chair.prefab"); GameObject robotPrefab = Load(Root + "/Robot/ServingRobot.prefab"); GameObject staffPrefab = Load(Root + "/People/Staff.prefab"); GameObject guestPrefab = Load(Root + "/People/Guest.prefab"); GameObject waitingPrefab = Load(Root + "/People/WaitingGuest.prefab"); GameObject seatedPrefab = Load(Root + "/People/SeatedGuest.prefab"); GameObject seatedBodyPrefab = Load(SeatedBodyPrefabPath); if (floorPrefab == null || wallPrefab == null || benchPrefab == null || tablePrefab == null || passPrefab == null || aisleMarkPrefab == null || benchSeatPrefab == null || chairPrefab == null || robotPrefab == null || staffPrefab == null || guestPrefab == null || waitingPrefab == null || seatedPrefab == null) { Debug.LogError("[ServiceSim] prefab を用意できませんでした。止めます"); return; } // シーンの組み立て(置くだけ。中身は持たない) GameObject root = FindOrCreateRoot(scene, "ServiceSim"); GameObject storeGo = FindOrCreateChild(root.transform, "Store"); StoreBuilder builder = Ensure(storeGo); if (builder.layout == null) builder.layout = family; builder.floorPrefab = floorPrefab; builder.wallPrefab = wallPrefab; builder.benchPrefab = benchPrefab; builder.benchSeatPrefab = benchSeatPrefab; builder.benchBackPrefab = Load(StorePrefabDir + "/BenchBack.prefab"); builder.benchBodyPrefab = Load(StorePrefabDir + "/BenchBody.prefab"); builder.tablePrefab = tablePrefab; builder.chairPrefab = chairPrefab; builder.kitchenPassPrefab = passPrefab; builder.aisleMarkPrefab = aisleMarkPrefab; builder.robotFloorMaterial = matRobotFloor; builder.peopleFloorMaterial = matPeopleFloor; NavMeshBaker baker = Ensure(storeGo); baker.layout = builder.layout; baker.bakeRoot = storeGo.transform; baker.store = builder; baker.peopleRadius = peopleSpec.personRadius; // 人用の NavMesh を焼く半径 baker.registerBackArea = peopleSpec.registerBackArea; // 店員側の区画(客は入れない) GameObject peopleGo = FindOrCreateChild(root.transform, "People"); PeopleSpawner spawner = Ensure(peopleGo); spawner.spec = peopleSpec; spawner.store = builder; spawner.staffPrefab = staffPrefab; spawner.seatedGuestPrefab = seatedPrefab; spawner.seatedBodyPrefab = seatedBodyPrefab; spawner.service = serviceSpec; GameObject robotsGo = FindOrCreateChild(root.transform, "Robots"); robotsGo.transform.position = Vector3.zero; // 店の回し方(卓の空き・料理の待ち行列・仕事板)と、1組ごとの流れ GameObject svcGo = FindOrCreateChild(root.transform, "Service"); ServiceOps opsC = Ensure(svcGo); opsC.spec = serviceSpec; opsC.robotSpec = robotSpec; opsC.store = builder; GroupManager groupsC = Ensure(svcGo); groupsC.spec = serviceSpec; groupsC.people = peopleSpec; groupsC.ops = opsC; groupsC.store = builder; groupsC.guestPrefab = guestPrefab; groupsC.waitingGuestPrefab = waitingPrefab; groupsC.bodyRoot = svcGo.transform; JamWatcher jamsC = Ensure(svcGo); jamsC.spec = peopleSpec; jamsC.ops = opsC; jamsC.store = builder; spawner.ops = opsC; GameObject expGo = FindOrCreateChild(root.transform, "Experiment"); ExperimentRunner runner = Ensure(expGo); runner.plan = plan; runner.service = serviceSpec; // サイトの試算の値(v5 3章)。無くてもこれまでの回し方はできる runner.simParams = AssetDatabase.LoadAssetAtPath(SimParamsPath); runner.ops = opsC; runner.groups = groupsC; runner.jams = jamsC; runner.store = builder; runner.baker = baker; runner.people = spawner; runner.robotSpec = robotSpec; runner.robotPrefab = robotPrefab; runner.robotsRoot = robotsGo.transform; runner.runOnPlay = false; // メニューから回すときだけ立てる EditorUtility.SetDirty(runner); EnsureLighting(root.transform); EnsureCameras(root.transform); EditorUtility.SetDirty(builder); EditorUtility.SetDirty(baker); EditorUtility.SetDirty(spawner); EditorUtility.SetDirty(opsC); EditorUtility.SetDirty(groupsC); EditorUtility.SetDirty(jamsC); EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene, ScenePath); // .asset に書かれていない項目を、いまの初期値で書きそろえる。 // すでに書かれている値は、読み込んだときにそのまま入っているので上書きされない。 string added = FillMissingAssetFields(new ScriptableObject[] { family, robotSpec, peopleSpec, plan, serviceSpec, AssetDatabase.LoadAssetAtPath(GridLayoutPath) }); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log("[ServiceSim] Setup おわり。" + "\n .asset に書き足した項目: " + (added.Length > 0 ? added : "なし") + "\n 描画方式: " + (CurrentPipeline() == null ? "Built-in" : CurrentPipeline().GetType().Name) + "\n 使うシェーダー: " + _shader.name + "(" + how + ")" + "\n シェーダーを直したマテリアル: " + _materialsFixed + " 個" + "\n NavMeshAgent を切った prefab: " + agentsDisabled + " 個" + "\n 当たりを外した通路板: " + collidersRemoved + " 個" + "\n 人の種類を直した prefab: " + kindsFixed + " 個" + (moved.Length > 0 ? "\n 前の版の店の設定を寄せました: " + moved : "") + "\n いまの店の形: " + builder.layout.name + "(" + builder.layout.StoreKindName + ")" + "\n 店の設定: " + FamilyLayoutPath + " / " + GridLayoutPath + "\n シーン: " + ScenePath + "(run_name=" + plan.runName + ")" + "\n ※ このシーンはビルド設定に足していません" + "\n 種類の印(Tag): " + SimTags.All.Length + " 種(新しく作った " + tagsAdded + " 種)"); } // ---------------- .asset の項目を書きそろえる ---------------- /// /// .asset に書かれていない項目を、いまの初期値で書き出す。 /// Unity は .asset を読むとき、書かれていない項目に C# の初期値を入れる。 /// そのまま SetDirty して保存すると、読み込んだ値はそのまま・足りない項目だけが増える。 /// /// これをしないと、設計書の「数字はすべて .asset に置く」が守られず、 /// 実際の値が .cs の初期値になってしまう。 /// 何を書き足したかを返す(報告に出す)。 /// static string FillMissingAssetFields(ScriptableObject[] assets) { List added = new List(); for (int i = 0; i < assets.Length; i++) { ScriptableObject a = assets[i]; if (a == null) continue; string path = AssetDatabase.GetAssetPath(a); if (string.IsNullOrEmpty(path)) continue; HashSet before = TopLevelKeys(path); EditorUtility.SetDirty(a); AssetDatabase.SaveAssets(); HashSet after = TopLevelKeys(path); foreach (string k in after) if (!before.Contains(k)) added.Add(a.name + "." + k); } if (added.Count == 0) return ""; added.Sort(); return string.Join(", ", added.ToArray()); } /// .asset(YAML)の、いちばん外側の項目の名前を集める static HashSet TopLevelKeys(string path) { HashSet keys = new HashSet(); if (!File.Exists(path)) return keys; string[] lines = File.ReadAllLines(path); for (int i = 0; i < lines.Length; i++) { string line = lines[i]; // MonoBehaviour の中身は半角スペース2つ下げ。さらに下げた行は中の要素なので見ない if (!line.StartsWith(" ") || line.StartsWith(" ")) continue; int colon = line.IndexOf(':'); if (colon <= 2) continue; string name = line.Substring(2, colon - 2).Trim(); if (name.Length == 0 || name.StartsWith("-")) continue; keys.Add(name); } return keys; } static UnityEngine.Rendering.RenderPipelineAsset CurrentPipeline() { return UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline; } // ---------------- 前の版の .asset を寄せる ---------------- /// /// StoreLayout_Family.asset が前の版(格子型)のままなら、Unity が見ないフォルダへ寄せる。 /// 消さない。寄せたら、そのパスを返す。 /// static string MoveStaleFamilyLayoutAside() { if (!File.Exists(FamilyLayoutPath)) return ""; if (AssetDatabase.LoadAssetAtPath(FamilyLayoutPath) != null) return ""; try { Directory.CreateDirectory(OldDir); string dst = OldDir + "/StoreLayout_Family_v0_grid.asset"; int n = 1; while (File.Exists(dst)) { dst = OldDir + "/StoreLayout_Family_v0_grid_" + n.ToString() + ".asset"; n++; } File.Move(FamilyLayoutPath, dst); if (File.Exists(FamilyLayoutPath + ".meta")) File.Move(FamilyLayoutPath + ".meta", dst + ".meta"); AssetDatabase.Refresh(); return dst; } catch (Exception e) { Debug.LogError("[ServiceSim] 前の版の " + FamilyLayoutPath + " を寄せられませんでした。" + "手で消すか動かしてから、もう一度 Setup を実行してください\n" + e.Message); return ""; } } // ---------------- フォルダ ---------------- static void EnsureFolders() { EnsureFolder("Assets", "ServiceSim"); EnsureFolder(Root, "Scenes"); EnsureFolder(Root, "Store"); EnsureFolder(Root + "/Store", "Prefabs"); EnsureFolder(Root + "/Store", "Materials"); EnsureFolder(Root, "Robot"); EnsureFolder(Root, "People"); EnsureFolder(Root, "Experiment"); EnsureFolder(Root, "Service"); EnsureFolder(Root, "Recording"); } static void EnsureFolder(string parent, string name) { string path = parent + "/" + name; if (!AssetDatabase.IsValidFolder(path)) AssetDatabase.CreateFolder(parent, name); } // ---------------- 設定ファイル ---------------- static T LoadOrCreate(string path) where T : ScriptableObject { T asset = AssetDatabase.LoadAssetAtPath(path); if (asset != null) return asset; asset = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(asset, path); return asset; } // ---------------- 見た目 ---------------- /// /// マテリアルを用意する。新しく作るときは色も付ける。 /// すでにあるときはシェーダーだけ確かめて、合っていなければ直し、 /// そのときだけ色も付け直す(手で変えた色を毎回消さないため)。 /// static Material Mat(string name, Color color) { string path = MatDir + "/" + name + ".mat"; Material mat = AssetDatabase.LoadAssetAtPath(path); if (mat == null) { mat = new Material(_shader); ServiceSimRender.ApplyLook(mat, color); AssetDatabase.CreateAsset(mat, path); return mat; } bool wrong = mat.shader == null || mat.shader != _shader || mat.shader.name == ServiceSimRender.ErrorShaderName || !mat.shader.isSupported; if (wrong) { mat.shader = _shader; ServiceSimRender.ApplyLook(mat, color); EditorUtility.SetDirty(mat); _materialsFixed++; } return mat; } // ---------------- 部品 ---------------- /// /// 透けて光が乗るマテリアルを作る(床の塗り分け用)。 /// すでにあればシェーダーと色だけ合わせ直す(止まっているときと再生中で同じ見た目にする)。 /// static Material TransparentMat(string name, Color color) { string how; Shader sh = ServiceSimRender.FindTransparentLitShader(out how); string path = MatDir + "/" + name + ".mat"; Material mat = AssetDatabase.LoadAssetAtPath(path); if (mat == null) { mat = new Material(sh != null ? sh : _shader); AssetDatabase.CreateAsset(mat, path); } if (sh != null && mat.shader != sh) { mat.shader = sh; _materialsFixed++; } mat.color = color; if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color); // Standard しか無いときは、透過の設定も入れる if (mat.shader != null && mat.shader.name == "Standard") { mat.SetFloat("_Mode", 3f); mat.SetOverrideTag("RenderType", "Transparent"); mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetInt("_ZWrite", 0); mat.DisableKeyword("_ALPHATEST_ON"); mat.EnableKeyword("_ALPHABLEND_ON"); mat.renderQueue = 3000; } EditorUtility.SetDirty(mat); return mat; } static void EnsureBoxPrefab(string path, string name, Material mat, bool withCollider) { if (AssetDatabase.LoadAssetAtPath(path) != null) return; GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube); go.name = name; SetMaterial(go, mat); if (!withCollider) { Collider col = go.GetComponent(); if (col != null) UnityEngine.Object.DestroyImmediate(col); } PrefabUtility.SaveAsPrefabAsset(go, path); UnityEngine.Object.DestroyImmediate(go); } /// /// 当たりだけの箱(見た目を持たない)。ベンチの当たりの箱に使う。 /// 大きさ・位置・高さは今までのベンチと同じなので、NavMesh も前のセンサーも変わらない。 /// static void EnsureColliderOnlyBoxPrefab(string path, string name) { if (AssetDatabase.LoadAssetAtPath(path) != null) return; GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube); go.name = name; // 見た目だけ外す(当たりは残す) MeshRenderer mr = go.GetComponent(); if (mr != null) UnityEngine.Object.DestroyImmediate(mr); MeshFilter mf = go.GetComponent(); if (mf != null) UnityEngine.Object.DestroyImmediate(mf); PrefabUtility.SaveAsPrefabAsset(go, path); UnityEngine.Object.DestroyImmediate(go); } /// prefab のマテリアルを合わせ直す(色を1か所で変えられるように) static void SetPrefabMaterial(string path, Material mat) { if (mat == null || !File.Exists(path)) return; GameObject contents = PrefabUtility.LoadPrefabContents(path); if (contents == null) return; try { bool changed = false; MeshRenderer[] rs = contents.GetComponentsInChildren(true); for (int i = 0; i < rs.Length; i++) { if (rs[i].sharedMaterial == mat) continue; rs[i].sharedMaterial = mat; changed = true; } if (changed) PrefabUtility.SaveAsPrefabAsset(contents, path); } finally { PrefabUtility.UnloadPrefabContents(contents); } } /// prefab に種類の印(Tag)を付ける static void SetPrefabTag(string path, string tag) { if (string.IsNullOrEmpty(tag) || !File.Exists(path)) return; GameObject contents = PrefabUtility.LoadPrefabContents(path); if (contents == null) return; try { if (contents.tag == tag) return; contents.tag = tag; PrefabUtility.SaveAsPrefabAsset(contents, path); } catch (UnityException) { Debug.LogWarning("[ServiceSim] Tag \"" + tag + "\" を付けられませんでした: " + path); } finally { PrefabUtility.UnloadPrefabContents(contents); } } /// /// 種類の印(Tag)を ProjectSettings/TagManager.asset に足す。 /// Tag は物理にも NavMesh にも効かないので、測る数字は変わらない。 /// **Assets/ServiceSim の外に出るのはここだけ**(LABEL_RULES.md に書いてある)。 /// static int EnsureTags() { int added = 0; for (int i = 0; i < SimTags.All.Length; i++) { string tag = SimTags.All[i]; if (System.Array.IndexOf(UnityEditorInternal.InternalEditorUtility.tags, tag) >= 0) continue; UnityEditorInternal.InternalEditorUtility.AddTag(tag); added++; } return added; } static void EnsureChairPrefab(string path, Material mat) { if (AssetDatabase.LoadAssetAtPath(path) != null) return; GameObject go = GameObject.CreatePrimitive(PrimitiveType.Cube); go.name = "Chair"; SetMaterial(go, mat); NavMeshObstacle obstacle = go.AddComponent(); obstacle.shape = NavMeshObstacleShape.Box; obstacle.center = Vector3.zero; obstacle.size = Vector3.one; obstacle.carving = true; obstacle.carveOnlyStationary = false; obstacle.enabled = false; // 引かれているあいだだけ有効にする go.AddComponent(); PrefabUtility.SaveAsPrefabAsset(go, path); UnityEngine.Object.DestroyImmediate(go); } static void EnsureRobotPrefab(string path, RobotSpec spec, Material body, Material front) { if (AssetDatabase.LoadAssetAtPath(path) != null) return; float r = spec.Radius; float h = spec.height; GameObject go = new GameObject("ServingRobot"); GameObject bodyGo = GameObject.CreatePrimitive(PrimitiveType.Cylinder); bodyGo.name = "Body"; bodyGo.transform.SetParent(go.transform, false); bodyGo.transform.localPosition = new Vector3(0f, h * 0.5f, 0f); bodyGo.transform.localScale = new Vector3(spec.diameter, h * 0.5f, spec.diameter); UnityEngine.Object.DestroyImmediate(bodyGo.GetComponent()); SetMaterial(bodyGo, body); GameObject frontGo = GameObject.CreatePrimitive(PrimitiveType.Cube); frontGo.name = "FrontMark"; frontGo.transform.SetParent(go.transform, false); frontGo.transform.localPosition = new Vector3(0f, h * 0.86f, r * 0.9f); frontGo.transform.localScale = new Vector3(spec.diameter * 0.7f, 0.08f, 0.08f); UnityEngine.Object.DestroyImmediate(frontGo.GetComponent()); SetMaterial(frontGo, front); CapsuleCollider col = go.AddComponent(); col.radius = r; col.height = h; col.center = new Vector3(0f, h * 0.5f, 0f); NavMeshAgent agent = go.AddComponent(); agent.radius = r; agent.height = h; agent.speed = spec.maxSpeed; agent.acceleration = spec.acceleration; agent.angularSpeed = spec.turnSpeedDegPerSec; agent.obstacleAvoidanceType = ObstacleAvoidanceType.NoObstacleAvoidance; agent.avoidancePriority = spec.avoidancePriority; agent.autoRepath = false; agent.updatePosition = false; agent.updateRotation = false; agent.enabled = false; // 置いたあとに有効にする(警告を出さないため) ServingRobotController controller = go.AddComponent(); FrontSensor sensor = go.AddComponent(); sensor.spec = spec; sensor.owner = controller; controller.spec = spec; controller.sensor = sensor; PrefabUtility.SaveAsPrefabAsset(go, path); UnityEngine.Object.DestroyImmediate(go); } /// /// 人の見た目(胴+頭)を作る。立っている人も座っている客も同じ作りにする。 /// 当たり判定は付けない(親が持っているものには触れない)。 /// bottomY … 足もと(立っている人は 0、座っている客は座面の高さ) /// topY … 頭の先(立っている人は背の高さ、座っている客は 1.35 m) /// すでにある "Body" と "Head" は作り直す。ほかの子どもと親の部品には触れない。 /// static void BuildPersonLook(GameObject root, float radius, float bottomY, float topY, Material mat) { if (root == null) return; for (int i = root.transform.childCount - 1; i >= 0; i--) { Transform c = root.transform.GetChild(i); if (c.name == "Body" || c.name == "Head") UnityEngine.Object.DestroyImmediate(c.gameObject); } float span = Mathf.Max(0.2f, topY - bottomY); float headR = Mathf.Min(0.11f, span * 0.25f); float headCenter = topY - headR; float bodyH = Mathf.Max(0.1f, headCenter - bottomY); // 頭は胴に少し食い込ませる GameObject body = GameObject.CreatePrimitive(PrimitiveType.Capsule); body.name = "Body"; body.transform.SetParent(root.transform, false); body.transform.localPosition = new Vector3(0f, bottomY + bodyH * 0.5f, 0f); body.transform.localScale = new Vector3(radius * 2f, bodyH * 0.5f, radius * 2f); UnityEngine.Object.DestroyImmediate(body.GetComponent()); SetMaterial(body, mat); GameObject head = GameObject.CreatePrimitive(PrimitiveType.Sphere); head.name = "Head"; head.transform.SetParent(root.transform, false); head.transform.localPosition = new Vector3(0f, headCenter, 0f); head.transform.localScale = new Vector3(headR * 2f, headR * 2f, headR * 2f); UnityEngine.Object.DestroyImmediate(head.GetComponent()); SetMaterial(head, mat); } /// /// すでにある人の prefab の **見た目だけ** を胴+頭に取り替える。 /// 当たり判定・NavMeshAgent・PersonBody・よける処理・Tag には触れない。 /// もう胴+頭になっていれば何もしない。 /// static bool FixWalkerLook(string path, PeopleSpec spec, Material mat) { if (!File.Exists(path) || spec == null || mat == null) return false; GameObject contents = PrefabUtility.LoadPrefabContents(path); if (contents == null) return false; try { float r = spec.personRadius; float h = spec.personHeight; Transform head = contents.transform.Find("Head"); if (head != null && Mathf.Abs(head.localPosition.y + head.localScale.y * 0.5f - h) < 0.001f) return false; // もう同じ形 BuildPersonLook(contents, r, 0f, h, mat); PrefabUtility.SaveAsPrefabAsset(contents, path); return true; } finally { PrefabUtility.UnloadPrefabContents(contents); } } static void EnsureWalkerPrefab(string path, string name, PeopleSpec spec, Material mat, WalkerKind kind) { if (AssetDatabase.LoadAssetAtPath(path) != null) return; float r = spec.personRadius; float h = spec.personHeight; GameObject go = new GameObject(name); // 見た目は胴+頭(座っている客と同じ作り)。当たり判定はこの下で別に付ける BuildPersonLook(go, r, 0f, h, mat); CapsuleCollider col = go.AddComponent(); col.radius = r; col.height = h; col.center = new Vector3(0f, h * 0.5f, 0f); NavMeshAgent agent = go.AddComponent(); agent.radius = r; agent.height = h; agent.speed = kind == WalkerKind.Staff ? spec.staffSpeed : spec.guestSpeed; agent.acceleration = spec.acceleration; agent.angularSpeed = spec.angularSpeedDegPerSec; agent.stoppingDistance = spec.stoppingDistance; agent.obstacleAvoidanceType = spec.avoidanceQuality; agent.avoidancePriority = spec.avoidancePriority; agent.enabled = false; // 置いたあとに有効にする(警告を出さないため) PersonBody personBody = go.AddComponent(); personBody.radius = r; personBody.walksInAisle = kind != WalkerKind.Waiting; // 待合に立つ客は通路を歩かない personBody.kind = kind == WalkerKind.Staff ? PersonKind.Staff : kind == WalkerKind.Waiting ? PersonKind.Waiting : PersonKind.Guest; if (kind == WalkerKind.Staff) { StaffAgent a = go.AddComponent(); a.spec = spec; } else if (kind == WalkerKind.Guest) { GuestAgent a = go.AddComponent(); a.spec = spec; } else { WaitingGuest a = go.AddComponent(); a.spec = spec; } PrefabUtility.SaveAsPrefabAsset(go, path); UnityEngine.Object.DestroyImmediate(go); } /// /// 座っている客の形(見た目だけ)。立っている客と同じ黄色で、背を低くする。 /// 当たり判定・PersonBody・NavMeshAgent は付けない /// (NavMesh にも、前のセンサーにも、人の数の記録にも効かないようにする)。 /// /// /// 立っている人(スタッフ・歩く客・待合の客)の見た目を、胴+頭にそろえる。 /// Setup を押していなくても Build Store だけで揃うようにするための出口。 /// 取り替えるのは見た目(Renderer)だけで、 /// 当たり判定・NavMeshAgent・PersonBody・よける処理・Tag には触れない。 /// 作り直した prefab の数を返す。 /// public static int EnsurePersonLook(PeopleSpec spec) { if (spec == null) return 0; if (_shader == null) { string how; _shader = ServiceSimRender.FindOpaqueShader(out how); if (_shader == null) return 0; } int fixedCount = 0; fixedCount += FixWalkerLook(Root + "/People/Staff.prefab", spec, Mat("Mat_Staff", StaffColor)) ? 1 : 0; fixedCount += FixWalkerLook(Root + "/People/Guest.prefab", spec, Mat("Mat_Guest", GuestColor)) ? 1 : 0; fixedCount += FixWalkerLook(Root + "/People/WaitingGuest.prefab", spec, Mat("Mat_WaitingGuest", WaitingColor)) ? 1 : 0; if (fixedCount > 0) Debug.Log("[ServiceSim] 立っている人の見た目を胴+頭にそろえました(" + fixedCount + " 体ぶん・背の高さ " + spec.personHeight.ToString("0.##") + " m)。" + "当たり判定・NavMeshAgent・PersonBody は触っていません"); return fixedCount; } /// /// ベンチの見た目(座面・背もたれ)と、当たりだけの箱を用意して StoreBuilder に入れる。 /// Setup を押していなくても Build Store だけで揃うようにするための出口(v5)。 /// 当たりの箱の大きさ・位置・高さは今までのベンチと同じなので、 /// NavMesh も前のセンサーも、通れる床も卓までの道のりも変わらない。 /// public static bool EnsureBenchLook(StoreBuilder builder) { if (builder == null) return false; if (_shader == null) { string how; _shader = ServiceSimRender.FindOpaqueShader(out how); if (_shader == null) { Debug.LogError("[ServiceSim] 描画方式に合うシェーダーが見つかりません。" + "ベンチの見た目は作りませんでした(探した結果: " + how + ")"); return false; } } EnsureTags(); Material seatMat = Mat("Mat_BenchSeat", new Color(0.62f, 0.33f, 0.29f)); Material backMat = Mat("Mat_BenchBack", new Color(0.48f, 0.25f, 0.22f)); EnsureBoxPrefab(StorePrefabDir + "/BenchSeat.prefab", "BenchSeat", seatMat, false); EnsureBoxPrefab(StorePrefabDir + "/BenchBack.prefab", "BenchBack", backMat, false); EnsureColliderOnlyBoxPrefab(StorePrefabDir + "/BenchBody.prefab", "BenchBody"); SetPrefabMaterial(StorePrefabDir + "/BenchSeat.prefab", seatMat); SetPrefabMaterial(StorePrefabDir + "/BenchBack.prefab", backMat); SetPrefabTag(StorePrefabDir + "/BenchSeat.prefab", SimTags.Bench); SetPrefabTag(StorePrefabDir + "/BenchBack.prefab", SimTags.Bench); SetPrefabTag(StorePrefabDir + "/BenchBody.prefab", SimTags.Bench); SetPrefabTag(StorePrefabDir + "/Chair.prefab", SimTags.Chair); SetPrefabTag(Root + "/Robot/ServingRobot.prefab", SimTags.Robot); SetPrefabTag(Root + "/People/Staff.prefab", SimTags.Staff); SetPrefabTag(Root + "/People/Guest.prefab", SimTags.Guest); SetPrefabTag(Root + "/People/WaitingGuest.prefab", SimTags.Guest); SetPrefabTag(SeatedBodyPrefabPath, SimTags.SeatedGuest); bool changed = false; changed |= Assign(ref builder.benchSeatPrefab, StorePrefabDir + "/BenchSeat.prefab"); changed |= Assign(ref builder.benchBackPrefab, StorePrefabDir + "/BenchBack.prefab"); changed |= Assign(ref builder.benchBodyPrefab, StorePrefabDir + "/BenchBody.prefab"); if (changed) EditorUtility.SetDirty(builder); return true; } static bool Assign(ref GameObject field, string path) { GameObject want = AssetDatabase.LoadAssetAtPath(path); if (want == null || field == want) return false; field = want; return true; } /// /// 座っている客の形を用意して返す(Build Store からも呼ぶ)。 /// 10/21 の分では、この prefab を作る前から参照だけしていたので、席に何も出ていなかった。 /// public static GameObject EnsureSeatedBodyPrefab(PeopleSpec spec) { if (spec == null) return null; if (_shader == null) { string how; _shader = ServiceSimRender.FindOpaqueShader(out how); if (_shader == null) { Debug.LogError("[ServiceSim] 描画方式に合うシェーダーが見つかりません。" + "座っている客の形は作りませんでした(探した結果: " + how + ")"); return null; } } EnsureSeatedBodyPrefab(SeatedBodyPrefabPath, spec, Mat("Mat_Guest", GuestColor)); return AssetDatabase.LoadAssetAtPath(SeatedBodyPrefabPath); } static void EnsureSeatedBodyPrefab(string path, PeopleSpec spec, Material mat) { float r = spec.personRadius; float h = spec.seatedBodyHeight; // すでにあっても、高さを変えたら作り直す(PeopleSpec.seatedBodyHeight が正) GameObject had = AssetDatabase.LoadAssetAtPath(path); if (had != null) { // 胴と頭の2つになっていて、頭の先が h と合っていれば、そのまま使う Transform head0 = had.transform.Find("Head"); bool sameShape = had.transform.childCount == 2 && head0 != null && Mathf.Abs(head0.localPosition.y + head0.localScale.y * 0.5f - h) < 0.001f; if (sameShape) return; AssetDatabase.DeleteAsset(path); } // 座面の高さから立ち上がる胴と、その上の頭。h は床から頭の先までの高さ float seat = Mathf.Clamp(spec.seatedSeatHeight, 0f, h - 0.3f); GameObject go = new GameObject("SeatedGuestBody"); BuildPersonLook(go, r, seat, h, mat); PrefabUtility.SaveAsPrefabAsset(go, path); UnityEngine.Object.DestroyImmediate(go); } static void EnsureSeatedGuestPrefab(string path, PeopleSpec spec) { if (AssetDatabase.LoadAssetAtPath(path) != null) return; // 座っている客は、椅子を引く仕掛けだけ。見た目は持たない(計測に効かないため) GameObject go = new GameObject("SeatedGuest"); PersonBody body = go.AddComponent(); body.radius = spec.personRadius; body.walksInAisle = false; body.kind = PersonKind.Seated; SeatedGuest seated = go.AddComponent(); seated.spec = spec; PrefabUtility.SaveAsPrefabAsset(go, path); UnityEngine.Object.DestroyImmediate(go); } /// /// すでにある prefab を直す(NavMeshAgent を切る/当たりを外す)。直したら true。 /// 中身を開いて保存し直す公式の手順を使う。 /// static bool FixPrefab(string path, bool disableAgent, bool removeCollider) { if (!File.Exists(path)) return false; GameObject contents = PrefabUtility.LoadPrefabContents(path); if (contents == null) return false; bool changed = false; try { if (disableAgent) { NavMeshAgent agent = contents.GetComponent(); if (agent != null && agent.enabled) { agent.enabled = false; changed = true; } } if (removeCollider) { Collider[] cols = contents.GetComponentsInChildren(true); for (int i = 0; i < cols.Length; i++) { UnityEngine.Object.DestroyImmediate(cols[i]); changed = true; } } if (changed) PrefabUtility.SaveAsPrefabAsset(contents, path); } finally { PrefabUtility.UnloadPrefabContents(contents); } return changed; } /// すでにある人の prefab に、人の種類を入れ直す。直したら true static bool FixPersonKind(string path, PersonKind kind) { if (!File.Exists(path)) return false; GameObject contents = PrefabUtility.LoadPrefabContents(path); if (contents == null) return false; bool changed = false; try { PersonBody body = contents.GetComponent(); if (body != null && body.kind != kind) { body.kind = kind; changed = true; } if (changed) PrefabUtility.SaveAsPrefabAsset(contents, path); } finally { PrefabUtility.UnloadPrefabContents(contents); } return changed; } static GameObject Load(string path) { return AssetDatabase.LoadAssetAtPath(path); } static void SetMaterial(GameObject go, Material mat) { if (mat == null) return; Renderer rend = go.GetComponent(); if (rend != null) rend.sharedMaterial = mat; } // ---------------- シーンの骨 ---------------- static GameObject FindOrCreateRoot(Scene scene, string name) { GameObject[] roots = scene.GetRootGameObjects(); for (int i = 0; i < roots.Length; i++) if (roots[i].name == name) return roots[i]; GameObject go = new GameObject(name); SceneManager.MoveGameObjectToScene(go, scene); return go; } static GameObject FindOrCreateChild(Transform parent, string name) { Transform t = parent.Find(name); if (t != null) return t.gameObject; GameObject go = new GameObject(name); go.transform.SetParent(parent, false); return go; } static T Ensure(GameObject go) where T : Component { T c = go.GetComponent(); if (c == null) c = go.AddComponent(); return c; } static void EnsureLighting(Transform root) { GameObject lightsGo = FindOrCreateChild(root, "Lighting"); GameObject sunGo = FindOrCreateChild(lightsGo.transform, "Sun"); Light sun = Ensure(sunGo); sun.type = LightType.Directional; sun.intensity = 1.1f; sun.shadows = LightShadows.Soft; sun.color = new Color(1f, 0.98f, 0.94f); sunGo.transform.rotation = Quaternion.Euler(52f, -34f, 0f); GameObject fillGo = FindOrCreateChild(lightsGo.transform, "Fill"); Light fill = Ensure(fillGo); fill.type = LightType.Directional; fill.intensity = 0.35f; fill.shadows = LightShadows.None; fill.color = new Color(0.85f, 0.9f, 1f); fillGo.transform.rotation = Quaternion.Euler(28f, 160f, 0f); RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Flat; RenderSettings.ambientLight = new Color(0.46f, 0.47f, 0.50f); RenderSettings.fog = false; } static void EnsureCameras(Transform root) { GameObject camsGo = FindOrCreateChild(root, "Cameras"); GameObject mainGo = FindOrCreateChild(camsGo.transform, "Main Camera"); Camera main = Ensure(mainGo); main.clearFlags = CameraClearFlags.SolidColor; main.backgroundColor = new Color(0.13f, 0.14f, 0.17f); mainGo.tag = "MainCamera"; GameObject topGo = FindOrCreateChild(camsGo.transform, "Cam_Top"); Camera top = Ensure(topGo); top.enabled = false; top.clearFlags = CameraClearFlags.SolidColor; top.backgroundColor = new Color(0.13f, 0.14f, 0.17f); GameObject obliqueGo = FindOrCreateChild(camsGo.transform, "Cam_Oblique"); Camera oblique = Ensure(obliqueGo); oblique.enabled = false; oblique.clearFlags = CameraClearFlags.SolidColor; oblique.backgroundColor = new Color(0.13f, 0.14f, 0.17f); // SRP が有効なときに必要な付属データを先に付けておく EnsureSrpCameraData(mainGo); EnsureSrpCameraData(topGo); EnsureSrpCameraData(obliqueGo); } /// /// URP / HDRP のカメラ付属データを、型名で探して付ける。 /// URP が入っていなくてもコンパイルできるように、直接の参照は持たない。 /// static void EnsureSrpCameraData(GameObject camGo) { if (UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline == null) return; string[] typeNames = { "UnityEngine.Rendering.Universal.UniversalAdditionalCameraData, Unity.RenderPipelines.Universal.Runtime", "UnityEngine.Rendering.HighDefinition.HDAdditionalCameraData, Unity.RenderPipelines.HighDefinition.Runtime" }; for (int i = 0; i < typeNames.Length; i++) { Type t = Type.GetType(typeNames[i]); if (t == null) continue; if (camGo.GetComponent(t) == null) camGo.AddComponent(t); return; } } } }