using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; namespace ServiceSim { /// /// 店の形から NavMesh を焼く。com.unity.ai.navigation を入れずに、 /// 組み込みの UnityEngine.AI の低レベル API だけで焼く(NavMeshSurface の代わり)。 /// /// 焼く対象は bakeRoot の下だけ。人とロボットは別の親の下に置くので、 /// 障害物として焼き込まれない。 /// 数字(半径・高さ・細かさ)は StoreLayout の .asset に置く。 /// public class NavMeshBaker : MonoBehaviour { [Header("設定")] public StoreLayout layout; [Tooltip("この下にあるコライダーだけを焼く。ふつうは Store の親")] public Transform bakeRoot; [Tooltip("焼く範囲を、組み立てた店の広さから取る")] public StoreBuilder store; [Tooltip("人用の NavMesh を焼く半径(m)。PeopleSpec の人の半径に合わせる。" + "0 以下、または navAgentRadius 以上なら人用は焼かず、人もロボット用の面を歩く")] public float peopleRadius = 0.2f; [Tooltip("レジの内側(店員側)に付ける NavMesh の区画の番号。PeopleSpec に合わせる。" + "客の NavMeshAgent はこの区画を外すので、店員側に入れない。0 なら区切らない")] public int registerBackArea = 3; // v5b: 狭い通路の区画(narrowAisleArea)はやめた。 // NavMesh に区画を書き込むと、同じ面が細かく割れて道がつながらない卓が出たため // (⑦で 11 卓)。ロボットの道順はサイトの routes を受け取ってたどる。 // NavMesh は段階1 と同じ(レジの内側の区画だけ残す)。 NavMeshData _data; NavMeshDataInstance _instance; // ---- 人用の NavMesh(半径が人の分だけ小さい)---- // ロボット用(navAgentRadius)と人用(人の半径)を同時に置き、 // それぞれの通行者の種類(agentTypeID)で引き分ける。 NavMeshData _peopleData; NavMeshDataInstance _peopleInstance; int _peopleTypeId = -1; /// ロボット用の通行者の種類(既定の 0) public int RobotAgentTypeId { get { return 0; } } /// 人用の通行者の種類。焼いていなければロボット用と同じ public int PeopleAgentTypeId { get { return _peopleTypeId >= 0 ? _peopleTypeId : 0; } } /// 人用の NavMesh を置けているか public bool HasPeopleNavMesh { get { return _peopleInstance.valid; } } /// 人用の NavMesh を焼いた半径(m) public float PeopleRadius { get { return peopleRadius; } } /// その種類で経路を引くための絞り込み public NavMeshQueryFilter FilterFor(int agentTypeId) { NavMeshQueryFilter f = new NavMeshQueryFilter(); f.agentTypeID = agentTypeId; f.areaMask = NavMesh.AllAreas; return f; } readonly List _sources = new List(); readonly List _markups = new List(); public bool HasNavMesh { get { return _instance.valid; } } void OnDisable() { Remove(); } /// /// いまの店の形で焼き直す。人の半径を渡すと、人用の NavMesh も一緒に焼く。 /// 人の半径が 0 以下、またはロボットの半径以上なら、人用は焼かない(1枚だけ)。 /// public void Bake(float radius) { peopleRadius = radius; Bake(); } /// いまの店の形で焼き直す public void Bake() { if (layout == null) { Debug.LogError("[ServiceSim] NavMeshBaker に StoreLayout が入っていません"); return; } Transform root = bakeRoot != null ? bakeRoot : transform; _sources.Clear(); BuildMarkups(); NavMeshBuilder.CollectSources( root, ~0, // すべてのレイヤー(レイヤーを増やさないため) NavMeshCollectGeometry.PhysicsColliders, // コライダーの形で焼く 0, // 既定のエリア(Walkable) _markups, _sources); AddRegisterBackArea(); if (_sources.Count == 0) { Debug.LogError("[ServiceSim] 焼く形が見つかりません。先に Build Store を実行してください"); return; } NavMeshBuildSettings settings = BuildSettings(); Bounds bounds = BakeBounds(); // v5b: スクリプトの読み込み直し(ドメインリロード)をまたぐと、前に焼いた NavMesh が // システムに残ったままで、この baker の _instance では外せない。 // 2026-10-22 の ⑦ で、前の作り(狭い通路の区画入り)の面が残り、 // 道がつながらない卓が出た。焼く前に、入っているものを全部外す。 NavMesh.RemoveAllNavMeshData(); Remove(); _data = NavMeshBuilder.BuildNavMeshData(settings, _sources, bounds, Vector3.zero, Quaternion.identity); if (_data == null) { Debug.LogError("[ServiceSim] NavMesh を焼けませんでした"); return; } _data.name = "ServiceSim_NavMesh"; _instance = NavMesh.AddNavMeshData(_data); BakePeople(settings, bounds); } /// /// 人用の NavMesh を焼いて一緒に置く(半径だけが違う)。 /// 通行者の種類を1つ作り、人の NavMeshAgent にその番号を入れて使い分ける。 /// ロボットはこれまでどおり navAgentRadius の面を歩く。 /// void BakePeople(NavMeshBuildSettings robotSettings, Bounds bounds) { RemovePeople(); if (peopleRadius <= 0.0005f || peopleRadius >= layout.navAgentRadius - 0.0005f) return; if (_peopleTypeId < 0) { NavMeshBuildSettings made = NavMesh.CreateSettings(); _peopleTypeId = made.agentTypeID; } NavMeshBuildSettings s = robotSettings; s.agentTypeID = _peopleTypeId; s.agentRadius = peopleRadius; _peopleData = NavMeshBuilder.BuildNavMeshData(s, _sources, bounds, Vector3.zero, Quaternion.identity); if (_peopleData == null) { Debug.LogWarning("[ServiceSim] 人用の NavMesh を焼けませんでした。人もロボット用の面を歩きます"); return; } _peopleData.name = "ServiceSim_NavMesh_People"; _peopleInstance = NavMesh.AddNavMeshData(_peopleData); PeopleNav.SetPeopleAgentType(_peopleTypeId); } void RemovePeople() { PeopleNav.SetPeopleAgentType(0); if (_peopleInstance.valid) NavMesh.RemoveNavMeshData(_peopleInstance); _peopleInstance = new NavMeshDataInstance(); _peopleData = null; } /// 焼いた NavMesh を外す public void Remove() { if (_instance.valid) NavMesh.RemoveNavMeshData(_instance); _instance = new NavMeshDataInstance(); _data = null; RemovePeople(); } /// /// 壁・ベンチ・仕切り・卓・椅子・カウンターを Not Walkable にする印を作る。 /// 焼く形としては残るので通れないままだが、上面は歩ける場所にならない。 /// void BuildMarkups() { _markups.Clear(); if (store == null) return; int notWalkable = NavMesh.GetAreaFromName("Not Walkable"); if (notWalkable < 0) notWalkable = 1; IList roots = store.NotWalkableRoots; for (int i = 0; i < roots.Count; i++) { if (roots[i] == null) continue; NavMeshBuildMarkup m = new NavMeshBuildMarkup(); m.root = roots[i]; m.overrideArea = true; m.area = notWalkable; _markups.Add(m); } } /// 焼く範囲。組み立てた店の広さに余白を足す public Bounds BakeBounds() { if (store != null && store.Plan != null) return store.Plan.NavBounds(layout.navBoundsPadding); // 店がまだ組み立てられていないときは、この下のコライダーの広がりを使う Transform root = bakeRoot != null ? bakeRoot : transform; Collider[] cols = root.GetComponentsInChildren(false); if (cols.Length == 0) return new Bounds(Vector3.zero, new Vector3(20f, 4f, 20f)); Bounds b = cols[0].bounds; for (int i = 1; i < cols.Length; i++) b.Encapsulate(cols[i].bounds); b.Expand(layout.navBoundsPadding * 2f); return b; } /// /// レジの内側(店員側)を別の区画にする。 /// 形は足さず、その箱の中の区画の番号だけを変える(ModifierBox)。 /// 客の NavMeshAgent がこの区画を外して歩くので、客は店員側に入れない。 /// void AddRegisterBackArea() { if (registerBackArea <= 2) return; // 0・1・2 は決まった区画なので使わない if (store == null) return; PlanRect r = store.RegisterBackRect; if (r.IsEmpty) return; NavMeshBuildSource src = new NavMeshBuildSource(); src.shape = NavMeshBuildSourceShape.ModifierBox; src.transform = Matrix4x4.Translate(new Vector3(r.CenterX, 0.5f, r.CenterZ)); src.size = new Vector3(r.SizeX, 2f, r.SizeZ); src.area = registerBackArea; _sources.Add(src); } /// /// 通行半径を変えて焼き直し、その歩ける面の三角形を返す(見た目の塗り分けに使う)。 /// いま入れてある NavMesh を外してから焼き、読み終わったら元に戻すので、 /// 計算に使う NavMesh(navAgentRadius で焼いたもの)は変わらない。 /// public NavMeshTriangulation TriangulateFor(float agentRadius) { NavMeshTriangulation empty = new NavMeshTriangulation(); if (layout == null) return empty; Transform root = bakeRoot != null ? bakeRoot : transform; _sources.Clear(); BuildMarkups(); NavMeshBuilder.CollectSources(root, ~0, NavMeshCollectGeometry.PhysicsColliders, 0, _markups, _sources); AddRegisterBackArea(); if (_sources.Count == 0) return empty; NavMeshBuildSettings s = BuildSettings(); s.agentRadius = agentRadius; // いまの NavMesh を一時的に全部外す(CalculateTriangulation は入れてある全部を返すため)。 // v5b: 読み込み直しをまたいで残った面もここで外す(RemoveAllNavMeshData)。 // そうしないと、前の作りで焼いた面が混ざった三角形を読んでしまう。 bool had = _instance.valid; NavMeshData keep = _data; bool hadPeople = _peopleInstance.valid; NavMeshData keepPeople = _peopleData; NavMesh.RemoveAllNavMeshData(); _instance = new NavMeshDataInstance(); _peopleInstance = new NavMeshDataInstance(); NavMeshTriangulation tri = empty; NavMeshData tmp = NavMeshBuilder.BuildNavMeshData( s, _sources, BakeBounds(), Vector3.zero, Quaternion.identity); NavMeshDataInstance tmpInst = new NavMeshDataInstance(); if (tmp != null) { tmpInst = NavMesh.AddNavMeshData(tmp); tri = NavMesh.CalculateTriangulation(); } if (tmpInst.valid) NavMesh.RemoveNavMeshData(tmpInst); // 元に戻す _data = keep; if (had && keep != null) _instance = NavMesh.AddNavMeshData(keep); _peopleData = keepPeople; if (hadPeople && keepPeople != null) _peopleInstance = NavMesh.AddNavMeshData(keepPeople); return tri; } NavMeshBuildSettings BuildSettings() { NavMeshBuildSettings s = NavMesh.GetSettingsByID(0); s.agentRadius = layout.navAgentRadius; s.agentHeight = layout.navAgentHeight; s.agentClimb = layout.navAgentClimb; s.agentSlope = layout.navAgentSlope; s.overrideVoxelSize = true; s.voxelSize = layout.navVoxelSize; s.minRegionArea = layout.navMinRegionArea; return s; } /// /// 受け渡し口から1点までの NavMesh 上の道のり(m)。 /// つながっていなければ -1 を返す。 /// public static float PathLength(Vector3 from, Vector3 to, float sampleRadius, out string status) { NavMeshQueryFilter f = new NavMeshQueryFilter(); f.agentTypeID = 0; f.areaMask = NavMesh.AllAreas; return PathLength(from, to, sampleRadius, f, out status); } /// 通行者の種類を指定して道のりを測る(人用・ロボット用を分けて確かめる) public static float PathLength(Vector3 from, Vector3 to, float sampleRadius, NavMeshQueryFilter filter, out string status) { NavMeshHit hitFrom, hitTo; if (!NavMesh.SamplePosition(from, out hitFrom, sampleRadius, filter)) { status = "出発点が NavMesh の上にない"; return -1f; } if (!NavMesh.SamplePosition(to, out hitTo, sampleRadius, filter)) { status = "行き先が NavMesh の上にない"; 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; } void OnDrawGizmosSelected() { if (layout == null) return; Bounds b = BakeBounds(); Gizmos.color = new Color(0.2f, 0.8f, 1f, 0.5f); Gizmos.DrawWireCube(b.center, b.size); } } }