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