using System.Collections.Generic; using UnityEngine; namespace ServiceSim { /// /// 客を「1組ごとに、入ってから出るまで」で動かす(試算の group() と同じ流れ)。 /// /// 入店 → 卓が空くまで待合(あふれたら店の外) → 案内 → 着席 → 料理 → 食事 /// (食事中にドリンクバー・トイレ・呼び出し) → 会計 → 退店 → 片付け /// /// 座っている人は人として置かない(通路に出ないため)。 /// 立ち上がった人だけを、使い回しの体(Guest.prefab)で歩かせる。 /// /// この script は人だけを扱う。卓の空きと料理の待ち行列は ServiceOps に任せる。 /// 数字は ServiceSpec と PeopleSpec の .asset に置く。 /// public class GroupManager : MonoBehaviour { /// 1組の今の状態 public enum GState { Waiting, Guided, Seated, Eating, Paying, Left, Clearing, Done } /// 1組 public class Group { public int id; public int size; public GState state = GState.Waiting; public TableSlot table; public float arriveS = -1f; public float guideStartS = -1f; public float seatedS = -1f; /// /// その組だけの乱数(依頼 v8b の B)。 /// 種と組の番号から作るので、**いつ引いても同じ組なら同じ値**になる。 /// 人数・料理ができるまで・食べる時間・割れ物の数・呼び出し・ /// ドリンクバー・トイレは、ぜんぶここから決まった順に引く。 /// 動きが1か所ずれても、ほかの組の中身は変わらない /// public System.Random rng; /// 食べ始めた時刻(最初の料理が全部届いた時刻, 秒)。まだなら -1(依頼 v8 の 2) public float eatStartS = -1f; public float eatEndS = -1f; public float payS = -1f; public float leftS = -1f; public bool visibleWaiting; public bool warmup; // 慣らしのために最初から座っている組 public readonly List dishes = new List(); public readonly List cookAt = new List(); // 受け渡し口に出す時刻 public readonly List cookFragile = new List(); public readonly List errandAt = new List(); // 立ち上がる時刻 public readonly List errandKind = new List(); // 0=ドリンクバー 1=トイレ public readonly List callAt = new List(); public int activeErrands; public int dishesOrdered; /// 何人目が立ったか。いつも同じ人が同じ椅子を引くようにする(B6) public int chairTurn; /// 会計・片付けで止まったままの秒数(B1。長びいたら卓を空ける) public float stuckS; /// /// 卓は空いたのに片付けが終わらず待った秒数(v5 3章・blocked_wait_mean)。 /// サイトの試算(haizen_sim.py の _acc)と同じ数え方で、 /// 「片付け待ちの卓の数だけ、待っている列の前から順に、そのコマの時間を足す」。 /// public float blockedWaitS; public GuestAgent waitingBody; // 待合に立たせている1人 // ---- 会計(レジに立つのは組の1人だけ)---- /// 会計に立つ1人の体。ほかの人は席に座ったまま public GuestAgent payerBody; /// 取ったレジの並ぶ点。-1 なら取れていない(席で待つ) public int payerSpot = -1; /// 会計の人をレジへ送ったか public bool payerSent; /// 会計を始めた時刻(食べ終わって Paying に入った時刻) public float payStartS = -1f; public StaffJob job; // いま出している仕事 } [Header("設定")] public ServiceSpec spec; public PeopleSpec people; public ServiceOps ops; public StoreBuilder store; [Tooltip("卓ごとの椅子の出し入れを読む先(B6)。無くても動く")] public PeopleSpawner spawner; [Header("部品")] [Tooltip("歩く客の体。待合の客にも使い回す")] public GameObject guestPrefab; [Tooltip("使っていない(段階4の場面の再現用に残してある)")] public GameObject waitingGuestPrefab; [Tooltip("歩く人を置く親")] public Transform bodyRoot; [Header("いまの様子(読むだけ)")] [SerializeField] int _groupsArrived; [SerializeField] int _groupsSeated; [SerializeField] int _groupsDone; [SerializeField] int _waitingGroups; [SerializeField] int _walkingBodies; readonly List _groups = new List(); readonly List _waiting = new List(); readonly List _pool = new List(); readonly List _live = new List(); PeopleTargets _targets; /// /// 客の中身でないもの(待つ場所を選ぶなど)に使う乱数。 /// 来客の間隔は _arrivalRng、組ごとのことは Group.rng(依頼 v8b の B) /// System.Random _rng; /// /// 来客の間隔だけの乱数(依頼 v8b の B)。 /// ほかの引きと混ぜないので、動きがずれても **来る組と来る時刻は変わらない** /// System.Random _arrivalRng; /// 組ごとの乱数を作るもとの種 int _seed; float _nextArrivalS; int _nextGroupId = 1; CrowdLevel _crowd; public IList Groups { get { return _groups; } } public int GroupsArrived { get { return _groupsArrived; } } public int GroupsSeated { get { return _groupsSeated; } } public int GroupsDone { get { return _groupsDone; } } public int WaitingGroups { get { return _waitingGroups; } } /// いま通路を歩いている人の数(スタッフを除く) public int WalkingBodies { get { return _walkingBodies; } } // ---------------- 用意 ---------------- /// /// 来客を受け付ける長さ(店の中の秒)。これを過ぎたら新しい組は来ない(v5 3章)。 /// サイトの決まりは「空の店から始め、peak_min だけ来客し、全員が帰るまで回す」。 /// 0 以下なら、これまでどおり止めずに来客し続ける。 /// public float arrivalsEndS; /// /// 慣らしのために、始めから食事中の組で卓を埋めるか(v5 3章)。 /// サイトと同じ「空の店から」にするときは false。 /// public bool seedWarmupGroups = true; /// まだ店の中にいる組があるか(全員が帰ったかの判定。v5 3章) public bool AnyGroupInStore { get { if (_waiting.Count > 0) return true; for (int i = 0; i < _groups.Count; i++) if (_groups[i].state != GState.Done) return true; return false; } } public void Setup(CrowdLevel crowd, int seed, PeopleTargets targets) { _crowd = crowd; _seed = seed; _rng = new System.Random(seed * 7919 + 13); _arrivalRng = new System.Random(seed * 7919 + 101); // 来客だけの列(依頼 v8b の B) _targets = targets; _groups.Clear(); _waiting.Clear(); ReturnAllBodies(); _groupsArrived = 0; _groupsSeated = 0; _groupsDone = 0; _nextGroupId = 1; float rate = spec.ArrivalsPerHour(crowd) / 3600f; _nextArrivalS = ServiceSpec.ExponentialSec(_arrivalRng, rate); if (seedWarmupGroups) SeedWarmupGroups(); } /// /// 慣らしのために、卓の一部を「食事の途中の組」で埋めて始める。 /// 空の店から始めると、はじめの数十分が本番と違う形になるため。 /// void SeedWarmupGroups() { if (people == null || ops == null) return; int tables = ops.Tables.Count; int want = Mathf.RoundToInt(tables * Mathf.Clamp01(people.warmupOccupancy)); int want0 = want; int placed = 0; for (int i = 0; i < tables && want > 0; i++) { TableSlot slot = ops.TakeTableAt(i, _nextGroupId); if (slot == null) continue; Group g = new Group(); g.id = _nextGroupId++; g.rng = NewGroupRng(g.id); // その組だけの乱数(依頼 v8b の B) // 2人席に3人以上を座らせない。卓の席数で頭を切る g.size = Mathf.Min(spec.PickGroupSize(g.rng), Mathf.Max(1, slot.seatCount)); g.table = slot; g.warmup = true; g.arriveS = 0f; g.seatedS = 0f; // 食事のどの時点かを決める(B3)。 // 0.00〜0.25:座ったばかりで料理を待っている(1皿めも2皿めもまだ) // 0.25〜0.50:1皿めは届いたが2皿めを待っている // 0.50〜1.00:受け取って食べている〜食べ終わりかけ // でたらめにすると食べ終わりかけが固まり、そのぶんの会計がいちどに来て // レジが詰まるので、均等にばらす double phase = (placed + 0.5) / Mathf.Max(1, want0); if (phase < 0.25) SeedWaitingFirstDishes(g, 2); else if (phase < 0.5) SeedWaitingFirstDishes(g, 1); else SeedEating(g, (float)((phase - 0.5) / 0.5)); placed++; _groups.Add(g); _groupsArrived++; _groupsSeated++; want--; } } /// /// 慣らし:座ったばかりで料理を待っている組(B3)。 /// まだ届いていない皿は、作る時間の残りをでたらめに決めて受け渡し口の待ち行列に入る。 /// void SeedWaitingFirstDishes(Group g, int pending) { g.state = GState.Seated; g.dishesOrdered = 2; // 1皿め・2皿めの作る時間。pending が 1 なら1皿めは届いた扱いにする float cook1 = ServiceSpec.TriangularSec(g.rng, spec.firstDish1CookMin); float cook2 = ServiceSpec.TriangularSec(g.rng, spec.firstDish2CookMin); if (pending >= 2) { g.cookAt.Add((float)g.rng.NextDouble() * cook1); g.cookFragile.Add(false); } else { // 1皿めは届いている g.dishes.Add(ops.SeedArrivedDish(g.id, g.table)); } g.cookAt.Add((float)g.rng.NextDouble() * cook2); g.cookFragile.Add(false); } /// /// 慣らし:もう食べている組(B3)。 /// done は食事の進み具合(0 なら食べ始め、1 なら食べ終わりかけ)。 /// 均等にばらすので、食べ終わりがいちどに来ない。 /// void SeedEating(Group g, float done) { g.state = GState.Eating; g.dishesOrdered = 2; // 最初の料理は届いている扱い for (int k = 0; k < 2; k++) g.dishes.Add(ops.SeedArrivedDish(g.id, g.table)); float eat = ServiceSpec.TriangularSec(g.rng, spec.eatMin); g.eatEndS = eat * (1f - Mathf.Clamp01(done)); ScheduleDuringMeal(g, g.eatEndS); } // ---------------- 毎コマ ---------------- public void Tick(float dt) { if (spec == null || ops == null || _rng == null) return; float now = ops.NowS; Arrive(now); CountBlockedWait(dt); SeatWaiting(now); for (int i = 0; i < _groups.Count; i++) Advance(_groups[i], now, dt); _waitingGroups = _waiting.Count; _walkingBodies = _live.Count; } /// /// 「卓は空いたのに片付けが終わらず待った」分を数える(v5 3章・blocked_wait_mean)。 /// /// サイトの試算(haizen_sim.py の _acc)と同じ数え方にそろえた: /// 片付け待ちの卓の数だけ、待っている列の**前から順に**、そのコマの時間を足す。 /// 片付けが終わっていれば、その卓の数だけ先頭の組が座れていたはず、という数え方。 /// /// full_wait は「席待ち − blocked_wait」で出す(サイトと同じ)。 /// 卓は空いているのに案内が追いつかない分(guide_wait)は、 /// 案内の仕事が板に出てから取られるまでで測る(OnGuided で記録する)。 /// /// サイトの試算には卓の大きさが無いので、ここも席数を見ずに列の前から数える /// (2人席・4人席は Unity だけのもの)。 /// void CountBlockedWait(float dt) { int clearing = ops.ClearingTableCount; if (clearing <= 0) return; int n = Mathf.Min(clearing, _waiting.Count); for (int i = 0; i < n; i++) _waiting[i].blockedWaitS += dt; } /// 来客(指数分布。試算の arrivals() と同じ) void Arrive(float now) { // サイトの決まり:peak_min だけ来客し、そのあとは新しい組は来ない(v5 3章) if (arrivalsEndS > 0f && now >= arrivalsEndS) return; if (now < _nextArrivalS) return; float rate = spec.ArrivalsPerHour(_crowd) / 3600f; _nextArrivalS = now + ServiceSpec.ExponentialSec(_arrivalRng, rate); Group g = new Group(); g.id = _nextGroupId++; g.rng = NewGroupRng(g.id); // その組だけの乱数(依頼 v8b の B) g.size = spec.PickGroupSize(g.rng); g.arriveS = now; g.state = GState.Waiting; _groups.Add(g); _waiting.Add(g); _groupsArrived++; // 待合に立たせるのは上限まで。あふれた組は店の外で待つ扱い(人として置かない) if (CountVisibleWaiting() < spec.waitingVisibleMax) { g.visibleWaiting = true; g.waitingBody = BorrowBody(_targets.RandomWaitingSpot(_rng), true); if (g.waitingBody != null) g.waitingBody.Begin(people, GuestAgent.Errand.Waiting, g.waitingBody.transform.position, 0f, false, Vector3.zero, null); } } int CountVisibleWaiting() { int n = 0; for (int i = 0; i < _waiting.Count; i++) if (_waiting[i].visibleWaiting) n++; return n; } /// /// その組だけの乱数を作る(依頼 v8b の B)。 /// /// 種と組の番号だけで決まるので、**いつ作っても同じ組なら同じ列**になる。 /// 引く順番も組ごとに決まっている(人数 → 最初の2皿の作る時間 → 食べる時間 → /// 割れ物 → 呼び出し → ドリンクバー → トイレ)ので、 /// 動きが1か所ずれても、ほかの組の中身は変わらない。 /// /// 引く数の分布・決まり(ServiceSpec の値)は変えていない。 /// 変わったのは「どの乱数の列から引くか」だけ。 /// System.Random NewGroupRng(int groupId) { unchecked { int h = _seed * 73856093; h ^= groupId * 19349663; h = h * 83492791 + 0x5bd1e995; return new System.Random(h & 0x7fffffff); // 負の種を避ける } } /// /// 空いた卓に、待っている組を先着順で入れる。 /// 2人席と4人席があるので、先頭の組が入れる卓が無いときは、 /// その組を飛ばして次の組を見る(4人の組が先頭にいるあいだ、 /// 2人席が空いているのに誰も座れない、を避ける)。 /// void SeatWaiting(float now) { while (_waiting.Count > 0 && ops.FreeTableCount > 0) { int pick = -1; for (int i = 0; i < _waiting.Count; i++) if (ops.HasFreeTableFor(_waiting[i].size)) { pick = i; break; } if (pick < 0) break; Group g = _waiting[pick]; TableSlot slot = ops.TakeFreeTable(g.id, g.size); if (slot == null) break; _waiting.RemoveAt(pick); g.table = slot; g.guideStartS = now; g.state = GState.Guided; ops.RecordSeatWait(g.arriveS); // スタッフが待合へ迎えに行き、卓まで案内する Group captured = g; StaffJob job = ops.PostJob(StaffJobKind.Guide, g.id, slot, null, 1); job.onDone = delegate { OnGuided(captured); }; g.job = job; } } /// /// その組の卓の椅子を1脚引く(B6)。人が動く瞬間だけ呼ぶ。 /// 何人目かを順に進めて、同じ人がいつも同じ椅子を引くようにする。 /// void PullChair(Group g, string reason) { if (spawner == null || g == null || g.table == null) return; SeatedGuest s = spawner.SeatedAt(g.table.index); if (s == null) return; s.PullFor(reason, g.chairTurn++); } void OnGuided(Group g) { // 入店待ちの内訳を1組ぶん残す(v5 3章。分け方はサイトの試算と同じ)。 // entry … 店に来てから卓に着くまで(案内の歩きと guide_s を含む) // seat … 店に来てから案内が始まるまで // blocked … そのうち、卓は空いたのに片付けが終わらず待った分 // guide … 案内の仕事が板に出てから、スタッフが取るまで // full = seat − blocked、staff = blocked + guide は ServiceOps で出す if (g.arriveS >= 0f && !g.warmup) { float entry = ops.NowS - g.arriveS; float seat = g.guideStartS >= 0f ? g.guideStartS - g.arriveS : 0f; float guideWait = g.job != null && g.job.takenS >= 0f ? Mathf.Max(0f, g.job.takenS - g.job.postedS) : 0f; ops.RecordEntryWait(entry, seat, g.blockedWaitS, guideWait); } g.job = null; g.state = GState.Seated; g.seatedS = ops.NowS; // 席に着くので椅子を引く(B6) PullChair(g, SeatedGuest.ReasonSit); // 案内された組は席に着く(待合の体を返す) if (g.waitingBody != null) { ReturnBody(g.waitingBody); g.waitingBody = null; } g.visibleWaiting = false; // 最初の注文(試算の first_dishes と同じ数・同じ作る時間) g.cookAt.Add(g.seatedS + ServiceSpec.TriangularSec(g.rng, spec.firstDish1CookMin)); g.cookFragile.Add(false); g.cookAt.Add(g.seatedS + ServiceSpec.TriangularSec(g.rng, spec.firstDish2CookMin)); g.cookFragile.Add(false); g.dishesOrdered = 2; } /// 食事中に起きること(デザート・呼び出し・ドリンクバー・トイレ)を先に決める void ScheduleDuringMeal(Group g, float eatEndS) { float now = ops.NowS; float eat = Mathf.Max(1f, eatEndS - now); int nFragile = ServiceSpec.PickCount(g.rng, spec.fragilePerGroup); for (int i = 0; i < nFragile; i++) { float at = now + (0.3f + 0.55f * (float)g.rng.NextDouble()) * eat; g.cookAt.Add(at + ServiceSpec.TriangularSec(g.rng, spec.fragileCookMin)); g.cookFragile.Add(true); g.dishesOrdered++; } // 呼び出し(試算は2回ぶん call_rate/2 で判定) for (int i = 0; i < 2; i++) if (g.rng.NextDouble() < spec.callRate / 2f) g.callAt.Add(now + (float)g.rng.NextDouble() * eat); // ドリンクバー(1人あたり)とトイレ(1組あたり) int drinks = ServiceSpec.PickCount(g.rng, spec.drinkTripsPerPerson * g.size); for (int i = 0; i < drinks; i++) { g.errandAt.Add(now + (float)g.rng.NextDouble() * eat); g.errandKind.Add(0); } int toilets = ServiceSpec.PickCount(g.rng, spec.toiletTripsPerGroup); for (int i = 0; i < toilets; i++) { g.errandAt.Add(now + (float)g.rng.NextDouble() * eat); g.errandKind.Add(1); } } void Advance(Group g, float now, float dt) { // できた料理を受け渡し口に出す for (int i = g.cookAt.Count - 1; i >= 0; i--) { if (now < g.cookAt[i]) continue; if (g.table != null) g.dishes.Add(ops.PutDish(g.id, g.table, g.cookFragile[i])); g.cookAt.RemoveAt(i); g.cookFragile.RemoveAt(i); } // 呼び出し for (int i = g.callAt.Count - 1; i >= 0; i--) { if (now < g.callAt[i]) continue; if (g.table != null) ops.PostJob(StaffJobKind.Call, g.id, g.table, null, 1); g.callAt.RemoveAt(i); } // 立ち上がってドリンクバー・トイレへ for (int i = g.errandAt.Count - 1; i >= 0; i--) { if (now < g.errandAt[i]) continue; StartErrand(g, g.errandKind[i]); g.errandAt.RemoveAt(i); g.errandKind.RemoveAt(i); } switch (g.state) { case GState.Seated: // 最初の料理が全部届いたら食事が始まる(試算の AllOf(firsts) と同じ) if (AllFirstDishesArrived(g)) { g.state = GState.Eating; g.eatStartS = now; // 食べ始めた時刻(依頼 v8 の 2) g.eatEndS = now + ServiceSpec.TriangularSec(g.rng, spec.eatMin); ScheduleDuringMeal(g, g.eatEndS); } break; case GState.Eating: if (now < g.eatEndS) break; if (g.cookAt.Count > 0 || !AllDishesArrived(g) || g.activeErrands > 0) break; g.state = GState.Paying; g.stuckS = 0f; g.payStartS = now; g.payerSent = false; g.payerSpot = -1; break; case GState.Paying: // 会計に立つのは組の1人だけ。レジの並ぶ点が空くまでは席で待つ SendPayerIfPossible(g, now); break; case GState.Left: // 退店したら、スタッフが片付けに行く(優先はいちばん後) g.state = GState.Clearing; g.stuckS = 0f; if (g.table != null) g.table.clearing = true; { Group captured = g; StaffJob job = ops.PostJob(StaffJobKind.Clear, g.id, g.table, null, 2); job.onDone = delegate { OnCleared(captured); }; g.job = job; } break; } // 座っている客の形を、いま席にいる人数に合わせる(見た目だけ) UpdateSeatedBodies(g); // 会計・片付けで止まって卓が空かないままなら、時間を見て卓を空ける(B1) if (g.state == GState.Paying || g.state == GState.Clearing) { g.stuckS += dt; if (spec.tableStuckFreeSec > 0f && g.stuckS >= spec.tableStuckFreeSec) ForceFreeTable(g); } } /// /// 会計・片付けが進まないまま時間が過ぎた組の卓を、強引に空ける(B1)。 /// これが多いときは、人が通れない店になっているしるし。 /// /// /// 座っている客の形を、いま席にいる人数に合わせる(見た目だけ)。 /// 席にいるのは「組の人数 − 立って歩いている人 − 会計に立っている人」。 /// 席を立つ・卓を空けると 0 になる。 /// void UpdateSeatedBodies(Group g) { if (spawner == null || g.table == null) return; SeatedGuest seated = spawner.SeatedAt(g.table.index); if (seated == null) return; bool sitting = g.state == GState.Seated || g.state == GState.Eating || g.state == GState.Paying; int n = 0; if (sitting) { n = g.size - g.activeErrands - (g.payerBody != null ? 1 : 0); if (n < 0) n = 0; } seated.ShowSeated(n); } void ForceFreeTable(Group g) { // 卓を空ける前に、座っている客の形を消す if (spawner != null && g.table != null) { SeatedGuest seated = spawner.SeatedAt(g.table.index); if (seated != null) seated.ShowSeated(0); } Vector3 at = g.table != null ? g.table.deliveryPoint : Vector3.zero; ops.AddGiveUp("組", g.state == GState.Paying ? "会計" : "片付け", g.table != null ? g.table.tableId : 0, at, at, g.stuckS, "卓を空けた"); // 出していた仕事を板から外す if (g.job != null) { ops.FinishJobSilently(g.job); g.job = null; } ReleasePayer(g); if (g.leftS < 0f) g.leftS = ops.NowS; ops.ReleaseTable(g.table, g.leftS); g.table = null; g.state = GState.Done; g.stuckS = 0f; _groupsDone++; } /// /// 会計に立つ1人をレジへ送る。 /// ・レジの並ぶ点が空いていなければ、席に座ったまま待つ(レジに団子にならないように) /// ・送れたときだけ、スタッフの会計の仕事を板に出す(誰も居ないレジで会計しないように) /// 残りの人は会計が終わってから、全員で立って店を出る(OnLeft)。 /// void SendPayerIfPossible(Group g, float now) { if (g.payerSent) return; // 前のとおり(誰もレジに立たず、スタッフだけで会計)に戻せるようにしておく。 // レジの前の人数を直す前・直したあとで比べるため if (!spec.payAtRegisterByOne) { g.payerSent = true; PullChair(g, SeatedGuest.ReasonPay); Group old = g; StaffJob j = ops.PostJob(StaffJobKind.Pay, g.id, g.table, null, 1); j.onDone = delegate { OnPaid(old); }; g.job = j; return; } Vector3 spot; int index = ops.TakeRegisterQueueSpot(out spot); if (index < 0) return; // 並ぶ点が埋まっている。席で待つ g.payerSpot = index; g.payerSent = true; // 会計で立ち上がるので椅子を引く(B6) PullChair(g, SeatedGuest.ReasonPay); Vector3 from = g.table != null ? g.table.deliveryPoint : _targets.entrance; g.payerBody = BorrowBody(from, false); if (g.payerBody != null) { g.payerBody.SetTable(g.table != null ? g.table.tableId : 0); g.payerBody.Begin(people, GuestAgent.Errand.Pay, spot, float.MaxValue, false, from, null); } Group captured = g; StaffJob job = ops.PostJob(StaffJobKind.Pay, g.id, g.table, null, 1); job.onDone = delegate { OnPaid(captured); }; g.job = job; } /// 会計の人を席へ戻し、レジの並ぶ点を空ける void ReleasePayer(Group g) { if (g.payerBody != null) { ReturnBody(g.payerBody); g.payerBody = null; } if (g.payerSpot >= 0) { ops.ReleaseRegisterQueueSpot(g.payerSpot); g.payerSpot = -1; } g.payerSent = false; } void OnPaid(Group g) { ReleasePayer(g); g.job = null; g.payS = ops.NowS; g.leftS = ops.NowS; g.state = GState.Left; g.stuckS = 0f; // 退店するので椅子を引く(B6) PullChair(g, SeatedGuest.ReasonLeave); // 組の全員が立って入口から出る int tableId = g.table != null ? g.table.tableId : 0; for (int i = 0; i < g.size; i++) { Vector3 from = g.table != null ? g.table.deliveryPoint : _targets.entrance; GuestAgent a = BorrowBody(from, false); if (a == null) break; GuestAgent captured = a; a.SetTable(tableId); a.Begin(people, GuestAgent.Errand.Leave, _targets.entrance, 0f, false, Vector3.zero, delegate { ReturnBody(captured); }); } } void OnCleared(Group g) { g.job = null; ops.ReleaseTable(g.table, g.leftS); g.state = GState.Done; _groupsDone++; } void StartErrand(Group g, int kind) { if (g.table == null) return; Vector3 home = g.table.deliveryPoint; Vector3 target = kind == 1 ? _targets.toilet : _targets.drinkBar; float dwell = kind == 1 ? spec.toiletSec : spec.drinkBarSec; // ドリンクバーは、空いている機械の前へ行く。埋まっていたら端に並ぶ。 // それも埋まっていたら、立ち上がらずにあきらめる(B4) int token = -1; if (kind == 0 && ops.HasDrinkBarSpots) { Vector3 spot; bool queued; token = ops.TakeDrinkBarSpot(out spot, out queued); if (token < 0) { ops.AddGiveUp("客", "ドリンクバー", g.table.tableId, target, home, 0f, "あきらめた"); return; } target = spot; } GuestAgent a = BorrowBody(home, false); if (a == null) { ops.ReleaseDrinkBarSpot(token, 0f); return; } // 立ち上がるので椅子を引く(B6) PullChair(g, kind == 1 ? SeatedGuest.ReasonToilet : SeatedGuest.ReasonDrinkBar); g.activeErrands++; Group cg = g; GuestAgent ca = a; int ct = token; float startS = ops.NowS; a.SetTable(g.table.tableId); a.Begin(people, kind == 1 ? GuestAgent.Errand.Toilet : GuestAgent.Errand.DrinkBar, target, dwell, true, home, delegate { cg.activeErrands--; ops.ReleaseDrinkBarSpot(ct, ops.NowS - startS - dwell); PullChair(cg, SeatedGuest.ReasonSitBack); // 戻って座り直す(B6) ReturnBody(ca); }); } bool AllFirstDishesArrived(Group g) { if (g.cookAt.Count > 0) return false; if (g.dishes.Count < 2) return false; for (int i = 0; i < 2 && i < g.dishes.Count; i++) if (g.dishes[i].arrivedS < 0f) return false; return true; } bool AllDishesArrived(Group g) { for (int i = 0; i < g.dishes.Count; i++) if (g.dishes[i].arrivedS < 0f) return false; return true; } // ---------------- 歩く体の使い回し ---------------- GuestAgent BorrowBody(Vector3 at, bool waitingKind) { // 待合の客も同じ体を使う(GuestAgent が「待っている」用事を持っている) GameObject prefab = guestPrefab; if (prefab == null || bodyRoot == null) return null; GuestAgent a = null; for (int i = _pool.Count - 1; i >= 0; i--) { GuestAgent c = _pool[i]; _pool.RemoveAt(i); if (c == null) continue; a = c; break; } if (a == null) { GameObject go = Instantiate(prefab, bodyRoot); a = go.GetComponent(); if (a == null) { Destroy(go); return null; } } a.gameObject.SetActive(true); a.ops = ops; // あきらめたことを残す先(B1) // 人がロボットをよける仕掛け(prefab に無くても付ける) if (spawner != null) spawner.AttachRobotYield(a.gameObject, _nextGroupId * 31 + _live.Count); a.PlaceAt(at); _live.Add(a); return a; } void ReturnBody(GuestAgent a) { if (a == null) return; _live.Remove(a); a.Stop(); a.gameObject.SetActive(false); _pool.Add(a); } void ReturnAllBodies() { for (int i = _live.Count - 1; i >= 0; i--) ReturnBody(_live[i]); _live.Clear(); } public void ClearAll() { ReturnAllBodies(); for (int i = 0; i < _pool.Count; i++) if (_pool[i] != null) Destroy(_pool[i].gameObject); _pool.Clear(); _groups.Clear(); _waiting.Clear(); } } }