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