using System; using System.Collections.Generic; using UnityEngine; namespace ServiceSim { /// /// StoreLayout が作った部品の一覧(StorePlan)を見て、床・壁・卓・椅子・ベンチ・ /// カウンター・通路の色板を並べる。 /// /// この script は店だけを扱う。ロボットと人は動かさない。 /// 格子や帯の組み方は知らない(StorePlan だけを見る)。 /// public class StoreBuilder : MonoBehaviour { [Serializable] public class TableInfo { public int tableId; public SeatKind kind; public Transform root; public Vector3 center; public Vector3 deliveryPoint; // ロボットが止まる位置(通路の上) public Vector3 aisleEntrance; // その通路に入る前に待つ位置 public Bounds aisleZone; // 譲り合いの区画 public int aisleIndex = -1; /// 何人かけの卓か(2人席・4人席) public int seatCount = 4; public List aisleChairs = new List(); /// 席ごとの「人が立つ点」(席に入れるかの確かめに使う) public List spots = new List(); /// 人が実際に座る位置(座っている客の形を出す場所) public List seatPoints = new List(); } [Serializable] public class AisleInfo { public string name; public bool narrow; public Bounds zone; public Vector3 entrance; public float width; } [Header("設定(数字はすべてこの .asset の中)")] public StoreLayout layout; [Header("部品")] public GameObject floorPrefab; public GameObject wallPrefab; public GameObject benchPrefab; // ベンチと仕切り(高さ 1.1 m のもの) public GameObject benchSeatPrefab; // ベンチの座面の見た目(当たりなし) public GameObject benchBackPrefab; // ベンチの背もたれの見た目(当たりなし) public GameObject benchBodyPrefab; // ベンチの当たりの箱(見た目なし。NavMesh とセンサーはこれだけを見る) public GameObject tablePrefab; public GameObject chairPrefab; public GameObject kitchenPassPrefab; // カウンター類 public GameObject aisleMarkPrefab; // 通路の色板(当たりなし) [Header("床の塗り分け(見た目だけ)")] [Tooltip("ロボットの体が入れる床の色(水色)")] public Material robotFloorMaterial; [Tooltip("人だけが入れる床の色(薄い黄色)")] public Material peopleFloorMaterial; [Header("組み立てた結果(読むだけ)")] [SerializeField] List _tables = new List(); [SerializeField] List _aisles = new List(); [SerializeField] float _builtAisleWidth; [SerializeField] Vector2 _outerSize; readonly Dictionary _points = new Dictionary(); /// /// 同じ種類に点がいくつもある場所(ロボットの待機場所・ドリンクバーの立つ点など)。 /// _points には最初の1つしか入らないので、別に全部を持つ。 /// readonly Dictionary> _pointLists = new Dictionary>(); readonly List _notWalkableRoots = new List(); StorePlan _plan; /// /// 上面を歩ける場所にしてはいけないものの親(壁・ベンチ・仕切り・卓・椅子・カウンター)。 /// NavMeshBaker がここを Not Walkable にして焼く。 /// public IList NotWalkableRoots { get { return _notWalkableRoots; } } public IList Tables { get { return _tables; } } public IList Aisles { get { return _aisles; } } public float BuiltAisleWidth { get { return _builtAisleWidth; } } public Vector2 OuterSize { get { return _outerSize; } } public StorePlan Plan { get { return _plan; } } /// ロボットの道順(v5b。サイトが決めた曲がり角の点)。無ければ null public RouteBook Routes { get { return _plan != null ? _plan.routes : null; } } /// 場所の点。無ければ原点 public Vector3 PointOf(PlaceKind kind) { Vector3 p; if (_points.TryGetValue(kind, out p)) return p; return Vector3.zero; } public bool HasPoint(PlaceKind kind) { return _points.ContainsKey(kind); } /// その種類の場所の点をすべて返す(出てきた順)。無ければ空 public IList PointsOf(PlaceKind kind) { List list; if (_pointLists.TryGetValue(kind, out list)) return list; return new List(); } // ロボットが使う点 public Vector3 PassPoint { get { return PointOf(PlaceKind.FoodPass); } } public Vector3 ReturnPoint { get { return PointOf(PlaceKind.DishReturn); } } public Vector3 PantryPoint { get { return PointOf(PlaceKind.Pantry); } } /// ロボットの待機場所(B2)。受け渡し口の前とは別の点 public IList RobotStandbyPoints { get { return PointsOf(PlaceKind.RobotStandby); } } // 人が使う点 public Vector3 RegisterPoint { get { return PointOf(PlaceKind.Register); } } public Vector3 ToiletPoint { get { return PointOf(PlaceKind.Toilet); } } public Vector3 DrinkBarPoint { get { return PointOf(PlaceKind.DrinkBar); } } public Vector3 EntrancePoint { get { return PointOf(PlaceKind.Entrance); } } public Vector3 WaitingPoint { get { return PointOf(PlaceKind.Waiting); } } /// レジの前に並ぶ点(会計に立つ人が1人ずつ並ぶ) public IList RegisterQueuePoints { get { return PointsOf(PlaceKind.RegisterQueue); } } /// 手の空いたスタッフが待つ点(1人に1つ) public IList StaffStandbyPoints { get { return PointsOf(PlaceKind.StaffStandby); } } /// レジの内側(店員側)の広がり。無ければ空 public PlanRect RegisterBackRect { get { return _plan != null ? _plan.RectOf(PlaceKind.RegisterBack) : new PlanRect(0f, 0f, 0f, 0f); } } /// レジの内側(スタッフが会計に立つ点)。無ければレジの前の点 public Vector3 RegisterBackPoint { get { return HasPoint(PlaceKind.RegisterBack) ? PointOf(PlaceKind.RegisterBack) : RegisterPoint; } } /// 待合の広さ(待つ客を散らすのに使う) public Bounds WaitingArea { get; private set; } readonly List _refuges = new List(); /// 人がロボットをよけて下がれる場所(通路の外)。壁ぞいの卓のすき間など public IList Refuges { get { return _refuges; } } /// 床の塗り分けを入れる親(当たりなし。NavMesh には焼かれない) public Transform FloorPaintRoot { get { Transform t = transform.Find("FloorPaint"); if (t == null) { GameObject go = new GameObject("FloorPaint"); go.transform.SetParent(transform, false); t = go.transform; } return t; } } /// /// 床を塗り分け直す(見た目だけ)。 /// 水色=ロボットの体が入れる床、薄い黄色=人だけが入れる床。 /// NavMesh も当たりも変えない。 /// public FloorPaint.Result PaintFloor(NavMeshBaker baker, float personRadius) { FloorPaint.Result res = new FloorPaint.Result(); if (layout == null || baker == null || _plan == null) return res; Transform root = FloorPaintRoot; if (!layout.showFloorPaint) { FloorPaint.Clear(root); return res; } UnityEngine.AI.NavMeshTriangulation robotTri = UnityEngine.AI.NavMesh.CalculateTriangulation(); UnityEngine.AI.NavMeshTriangulation peopleTri = baker.TriangulateFor(personRadius); return FloorPaint.Rebuild(root, layout, _plan.NavBounds(layout.navBoundsPadding), robotTri, peopleTri, personRadius, _plan, robotFloorMaterial, peopleFloorMaterial); } /// /// その点がある場所の名前(レジ・待合・ドリンクバーなど)。どこでもなければ空。 /// 記録で「どこで起きたか」を通路の名前だけでなく場所の名前でも言うために使う。 /// public string PlaceNameAt(Vector3 p) { if (_plan == null) return ""; for (int i = 0; i < _plan.places.Count; i++) { PlanPlace pl = _plan.places[i]; if (pl.rect.IsEmpty) continue; if (pl.rect.Contains(p)) return PlaceName(pl.kind); } return ""; } /// 場所の種類の名前(記録に出す) public static string PlaceName(PlaceKind kind) { switch (kind) { case PlaceKind.Kitchen: return "厨房"; case PlaceKind.Pantry: return "パントリー"; case PlaceKind.FoodPass: return "受け渡し口"; case PlaceKind.DishReturn: return "返却口"; case PlaceKind.Entrance: return "入口"; case PlaceKind.Vestibule: return "風除室"; case PlaceKind.Waiting: return "待合"; case PlaceKind.Register: return "レジ"; case PlaceKind.Toilet: return "トイレ"; case PlaceKind.DrinkBar: return "ドリンクバー"; case PlaceKind.Office: return "事務所"; case PlaceKind.RobotStandby: return "ロボット待機"; case PlaceKind.DrinkBarStand: return "ドリンクバーの前"; case PlaceKind.DrinkBarQueue: return "ドリンクバーの列"; case PlaceKind.RegisterQueue: return "レジの列"; case PlaceKind.StaffStandby: return "スタッフ待機"; case PlaceKind.RegisterBack: return "レジの内側"; } return ""; } /// いまの layout.aisleWidthIndex で店を組み立て直す public void Build() { if (layout == null) { Debug.LogError("[ServiceSim] StoreBuilder に StoreLayout が入っていません"); return; } Clear(); _plan = layout.NewPlan(); if (_plan == null || _plan.seats.Count == 0) { Debug.LogError("[ServiceSim] StoreLayout が部品の一覧を作れませんでした(席が0)"); return; } _builtAisleWidth = _plan.aisleWidth; _outerSize = new Vector2(_plan.floor.SizeX, _plan.floor.SizeZ); BuildFloor(_plan); BuildBoxes(_plan); BuildPlaces(_plan); BuildSeats(_plan); BuildAisles(_plan); BuildAisleMarks(_plan); _refuges.AddRange(_plan.refuges); } /// 組み立てたものを消す public void Clear() { _tables.Clear(); _aisles.Clear(); _points.Clear(); _pointLists.Clear(); _refuges.Clear(); _notWalkableRoots.Clear(); _plan = null; WaitingArea = new Bounds(); for (int i = transform.childCount - 1; i >= 0; i--) DestroyChild(transform.GetChild(i).gameObject); } /// 細い通路の幅を選び直して組み立て直す public void SetAisleWidthIndex(int index) { if (layout == null) return; layout.aisleWidthIndex = index; Build(); } // ---------------- 床 ---------------- void BuildFloor(StorePlan plan) { Transform g = NewGroup("Floor", true); if (plan.floorPieces.Count == 0) { PlaceFloor(g, plan, plan.floor, "Floor"); return; } for (int i = 0; i < plan.floorPieces.Count; i++) PlaceFloor(g, plan, plan.floorPieces[i], "Floor" + (i + 1).ToString("00")); } void PlaceFloor(Transform parent, StorePlan plan, PlanRect r, string name) { GameObject floor = Box(floorPrefab, parent, name, r, plan.floorThickness); if (floor != null) floor.transform.position = new Vector3(r.CenterX, -plan.floorThickness * 0.5f, r.CenterZ); } // ---------------- 箱(壁・仕切り・ベンチ)---------------- void BuildBoxes(StorePlan plan) { Transform walls = NewGroup("Walls", false); Transform seats = NewGroup("SeatStructure", false); // 座る場所の見た目は、当たりを持たない prefab でしか置かない。 // 当たりのある prefab で代わりに置くと NavMesh が変わってしまうため、 // prefab が無いときは置かずに知らせる(Setup を実行すれば作られる)。 int skippedSeatLooks = 0; for (int i = 0; i < plan.boxes.Count; i++) { PlanBox b = plan.boxes[i]; // 見た目だけの箱は、当たりを持たない prefab でしか置かない。 // 当たりのある prefab で代えると NavMesh が変わってしまう bool lookOnly = b.style == BoxStyle.BenchSeat || b.style == BoxStyle.BenchBack; if (lookOnly && PrefabFor(b.style) == null) { skippedSeatLooks++; continue; } Transform parent = (b.style == BoxStyle.Bench || b.style == BoxStyle.Divider || b.style == BoxStyle.BenchBody || lookOnly) ? seats : walls; GameObject go = Box(PrefabFor(b.style), parent, b.name, b.rect, b.height); if (go == null) continue; if (b.baseY != 0f) go.transform.position += Vector3.up * b.baseY; SimTags.Apply(go, TagFor(b.style)); } if (skippedSeatLooks > 0) Debug.LogWarning("[ServiceSim] ベンチの見た目(座面・背もたれ)" + skippedSeatLooks + " 個を置きませんでした(BenchSeat.prefab / BenchBack.prefab がありません)。" + "ServiceSim/Setup を実行してください。" + "当たりの箱はそのまま置いているので、測る数字は変わりません"); } /// /// 種類の印(Tag)。あとで映像から見分ける学習に使う。いまは付けるだけ(v5)。 /// Tag は物理にも NavMesh にも効かないので、測る数字は変わらない /// static string TagFor(BoxStyle style) { switch (style) { case BoxStyle.Bench: case BoxStyle.BenchBody: case BoxStyle.BenchSeat: case BoxStyle.BenchBack: return SimTags.Bench; case BoxStyle.Table: return SimTags.Table; case BoxStyle.Chair: return SimTags.Chair; default: return null; } } // ---------------- 場所 ---------------- void BuildPlaces(StorePlan plan) { Transform g = NewGroup("Places", false); for (int i = 0; i < plan.places.Count; i++) { PlanPlace p = plan.places[i]; if (p.solid && p.height > 0.0005f) Box(PrefabFor(p.kind), g, p.name, p.rect, p.height); NewChild(g, p.name + "_Point").position = p.point; if (!_points.ContainsKey(p.kind)) _points.Add(p.kind, p.point); List list; if (!_pointLists.TryGetValue(p.kind, out list)) { list = new List(); _pointLists.Add(p.kind, list); } list.Add(p.point); if (p.kind == PlaceKind.Waiting) WaitingArea = p.rect.ToBounds(0f, 2f); } } // ---------------- 席 ---------------- void BuildSeats(StorePlan plan) { Transform g = NewGroup("Seats", false); for (int i = 0; i < plan.seats.Count; i++) { PlanSeat s = plan.seats[i]; Transform unit = NewChild(g, "Table" + s.tableId.ToString("00")); Box(tablePrefab, unit, "TableTop", s.tableRect, s.tableHeight); TableInfo info = new TableInfo(); info.tableId = s.tableId; info.kind = s.kind; info.root = unit; info.center = s.tableRect.Center; info.deliveryPoint = s.deliveryPoint; info.aisleIndex = s.aisleIndex; info.seatCount = s.seatCount; info.spots.AddRange(s.spots); info.seatPoints.AddRange(s.seatPoints); PlanAisle aisle = plan.AisleAt(s.aisleIndex); if (aisle != null) { info.aisleZone = aisle.Zone(-1f, 2f); info.aisleEntrance = aisle.entrance; } else { info.aisleEntrance = s.deliveryPoint; } for (int c = 0; c < s.chairs.Count; c++) { PlanChair pc = s.chairs[c]; GameObject go = Box(chairPrefab, unit, "Chair", pc.rect, pc.height); if (go == null) continue; Chair chair = go.GetComponent(); if (chair == null) continue; chair.isAisleSide = pc.pullable; chair.pullDirection = pc.pullDirection; chair.MarkParked(); if (pc.pullable) info.aisleChairs.Add(chair); } _tables.Add(info); } } // ---------------- 通路 ---------------- void BuildAisles(StorePlan plan) { for (int i = 0; i < plan.aisles.Count; i++) { PlanAisle a = plan.aisles[i]; AisleInfo info = new AisleInfo(); info.name = a.name; info.narrow = a.narrow; info.zone = a.Zone(-1f, 2f); info.entrance = a.entrance; info.width = Mathf.Min(a.rect.SizeX, a.rect.SizeZ); _aisles.Add(info); } } /// /// 通路の床に色の板を敷く。当たりは持たないので NavMesh には焼かれず、計測には効かない。 /// void BuildAisleMarks(StorePlan plan) { if (!layout.showAisleMarks || aisleMarkPrefab == null) return; Transform g = NewGroup("AisleMarks", false); for (int i = 0; i < plan.aisles.Count; i++) { PlanAisle a = plan.aisles[i]; Box(aisleMarkPrefab, g, "Mark_" + a.name, a.rect, layout.aisleMarkThickness); } } // ---------------- 小さな道具 ---------------- GameObject PrefabFor(BoxStyle style) { switch (style) { case BoxStyle.Floor: return floorPrefab; case BoxStyle.Table: return tablePrefab; case BoxStyle.Chair: return chairPrefab; case BoxStyle.Bench: return benchPrefab != null ? benchPrefab : wallPrefab; case BoxStyle.Divider: return benchPrefab != null ? benchPrefab : wallPrefab; // 当たりの箱(見た目なし)。prefab が無いときは、これまでどおり見える箱で代える // (当たりの大きさは同じなので、NavMesh と測る数字は変わらない) case BoxStyle.BenchBody: return benchBodyPrefab != null ? benchBodyPrefab : (benchPrefab != null ? benchPrefab : wallPrefab); case BoxStyle.BenchSeat: return benchSeatPrefab; // 無いときは BuildBoxes が置かない case BoxStyle.BenchBack: return benchBackPrefab; // 同じく case BoxStyle.Counter: return kitchenPassPrefab; case BoxStyle.AisleMark: return aisleMarkPrefab; default: return wallPrefab; } } GameObject PrefabFor(PlaceKind kind) { switch (kind) { case PlaceKind.Kitchen: case PlaceKind.Office: return wallPrefab; default: return kitchenPassPrefab; } } /// /// 組み立てたものを入れる親を作る。 /// walkable が false の親は、NavMesh で Not Walkable として焼く(上面に乗れないように)。 /// Transform NewGroup(string name, bool walkable) { Transform t = NewChild(transform, name); if (!walkable) _notWalkableRoots.Add(t); return t; } Transform NewChild(Transform parent, string name) { GameObject go = new GameObject(name); go.transform.SetParent(parent, false); return go.transform; } /// 長方形と高さから箱を1つ置く(下の面が y=0、上の面が y=height) GameObject Box(GameObject prefab, Transform parent, string name, PlanRect rect, float height) { if (rect.IsEmpty || height <= 0.0005f) return null; GameObject go; if (prefab != null) { go = Instantiate(prefab, parent); } else { go = GameObject.CreatePrimitive(PrimitiveType.Cube); go.transform.SetParent(parent, false); } go.name = name; go.transform.localScale = new Vector3(rect.SizeX, height, rect.SizeZ); go.transform.position = new Vector3(rect.CenterX, height * 0.5f, rect.CenterZ); return go; } static void DestroyChild(GameObject go) { if (Application.isPlaying) Destroy(go); else DestroyImmediate(go); } } }