using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; namespace ServiceSim { /// /// スタッフ1人。確率で仕事を選ぶのではなく、そのときに起きている仕事を /// 仕事板(ServiceOps)から優先度の順に取って片付ける。 /// 優先0:立ち往生したロボットを動かす(v5 3章・サイトの決まりで「ほかの仕事より先」) /// 優先1:案内・料理を運ぶ・呼び出し・会計 / 優先2:片付け /// 手が空いているときはパントリーの前で待つ。 /// /// この script は人だけを扱う。どの仕事が要るかは ServiceOps が決める。 /// 数字は ServiceSpec と PeopleSpec の .asset に置く。 /// [RequireComponent(typeof(NavMeshAgent))] public class StaffAgent : MonoBehaviour { /// 行き先1つぶん struct Step { public Vector3 point; public float dwellSec; public bool handDish; // ここで料理が届く } [Header("設定")] public PeopleSpec spec; public ServiceSpec service; [Tooltip("仕事板")] public ServiceOps ops; [Header("いまの様子(読むだけ)")] [SerializeField] string _jobText = "待機"; [SerializeField] int _step; [SerializeField] float _stepS; // いまの行き先へ歩き始めてからの秒数 NavMeshAgent _agent; PersonBody _body; // 歩いた距離を数えるのに使う PersonRobotYield _yield; // ロボットをよけている最中は「着いた」と見ない PeopleTargets _targets; StaffJob _job; readonly List _steps = new List(); float _timer; bool _dwelling; bool _idleParked; Vector3 _pending; bool _needsDestination; /// いまの仕事(頭の上の字幕が読む) public StaffJob CurrentJob { get { return _job; } } public string JobText { get { return _jobText; } } public int Step_ { get { return _step; } } /// いま向かっている点(頭の上の字幕が読む) public Vector3 TargetPoint { get { return _step >= 0 && _step < _steps.Count ? _steps[_step].point : transform.position; } } void Awake() { _agent = GetComponent(); _yield = GetComponent(); } /// ロボットをよけている最中か(行き先がよけ場所にすり替わっている) bool Yielding { get { if (_yield == null) _yield = GetComponent(); return _yield != null && _yield.Intervening; } } /// 何人目のスタッフか(待つ点を1人に1つ割り当てるのに使う) public int staffIndex; /// 手の空いたときに待つ点。無ければパントリーの前 Vector3 StandbyPoint { get { if (_targets == null) return transform.position; int n = _targets.staffStandby.Count; if (n == 0) return _targets.pantry; return _targets.staffStandby[((staffIndex % n) + n) % n]; } } public void Configure(PeopleSpec peopleSpec, ServiceSpec serviceSpec, ServiceOps serviceOps, PeopleTargets targets, int seed, int index) { staffIndex = index; spec = peopleSpec; service = serviceSpec; ops = serviceOps; _targets = targets; if (_agent == null) _agent = GetComponent(); ApplySpec(); _job = null; _steps.Clear(); _step = 0; _stepS = 0f; _dwelling = false; _idleParked = false; SetIgnoredRobot(null); _jobText = "待機"; } void ApplySpec() { if (_agent == null || spec == null) return; _agent.radius = spec.personRadius; _agent.height = spec.personHeight; _agent.speed = spec.staffSpeed; _agent.acceleration = spec.acceleration; _agent.angularSpeed = spec.angularSpeedDegPerSec; _agent.stoppingDistance = spec.stoppingDistance; _agent.obstacleAvoidanceType = spec.avoidanceQuality; _agent.avoidancePriority = spec.avoidancePriority; _agent.autoRepath = true; // 人は人用の NavMesh(半径 0.2 m)を歩く。ロボットは 0.25 m のまま PeopleNav.ApplyAgentType(_agent); // スタッフはレジの内側(店員側)に入れる _agent.areaMask = NavMesh.AllAreas; } void Update() { if (_agent == null || spec == null || service == null || ops == null || _targets == null) return; // 行き先に着けないまま時間が過ぎたら、仕事を板に戻す(B1)。 // NavMesh に乗れないときも時間が進むように、下で戻る前に数える if (_job != null && !_dwelling) { _stepS += Time.deltaTime; if (GiveUpIfLate()) return; } // 時間の使い方を残す(案内・料理・呼び出し・会計・片付け・パントリーで待つ) ops.AddStaffTime(_job != null ? JobKindName(_job.kind) : "待機", Time.deltaTime); // 歩いた距離を残す(walk_km_per_staff)。「移した」ぶんの跳びは ServiceOps 側で落とす if (_body == null) _body = GetComponent(); if (_body != null) ops.AddStaffWalk(_body.LastStepM); if (!PeopleNav.EnsureReady(transform, _agent)) return; if (_needsDestination && !PeopleNav.Go(_agent, _pending)) return; _needsDestination = false; if (_job == null) { Idle(); return; } if (_dwelling) { _timer += Time.deltaTime; if (_timer < _steps[_step].dwellSec) return; _dwelling = false; Deliver(_steps[_step]); _step++; if (_step >= _steps.Count) { Done(); return; } Go(_steps[_step].point); return; } // ロボットをよけている最中は、行き先がよけ場所にすり替わっているので // 「着いた」と見てはいけない if (Yielding) return; // 立ち往生を動かしに行くときは、相手が動いたら行き先を追いかけ、 // 着いたかどうかもその点との距離で見る(v5 3章) if (IsRescueStep()) { if (!UpdateRescueStep()) return; } else { if (_agent.pathPending) return; if (_agent.remainingDistance > _agent.stoppingDistance + 0.05f) return; } if (_steps[_step].dwellSec > 0f) { _dwelling = true; _timer = 0f; return; } Deliver(_steps[_step]); _step++; if (_step >= _steps.Count) { Done(); return; } Go(_steps[_step].point); } /// 手が空いているとき。次の仕事を取り、無ければパントリーの前で待つ void Idle() { StaffJob job = ops.TakeNextJob(); if (job == null) { if (!_idleParked) { _idleParked = true; _jobText = "待機"; Go(StandbyPoint); } return; } _idleParked = false; Begin(job); } /// 仕事の名前(時間の使い方の記録に使う) public static string JobKindName(StaffJobKind kind) { switch (kind) { case StaffJobKind.Guide: return "案内"; case StaffJobKind.Serve: return "料理"; case StaffJobKind.Call: return "呼び出し"; case StaffJobKind.Pay: return "会計"; case StaffJobKind.Clear: return "片付け"; case StaffJobKind.Rescue: return "立ち往生"; } return "そのほか"; } void Begin(StaffJob job) { _job = job; _steps.Clear(); _step = 0; _dwelling = false; _timer = 0f; switch (job.kind) { case StaffJobKind.Guide: _jobText = "案内"; Add(_targets.waiting, service.guideSec, false); Add(TablePoint(job), 0f, false); break; case StaffJobKind.Serve: _jobText = "料理を運ぶ"; Add(_targets.foodPass, 0f, false); Add(TablePoint(job), service.staffPlaceSec, true); Add(_targets.foodPass, 0f, false); break; case StaffJobKind.Call: _jobText = "呼び出し"; Add(TablePoint(job), service.callSec, false); break; case StaffJobKind.Pay: // スタッフはレジの内側(客と反対側。太い通路からだけ入れる)に立つ _jobText = "会計"; Add(_targets.registerBack, service.paySec, false); break; case StaffJobKind.Clear: // 皿をまとめ、返却口へ運び、戻って拭いて、もう一度運ぶ(bus_trips ぶん) _jobText = "片付け"; Add(TablePoint(job), service.clearSec, false); int trips = Mathf.Max(1, service.busTrips); for (int i = 0; i < trips; i++) { Add(_targets.dishReturn, 0f, false); if (i < trips - 1) Add(TablePoint(job), 0f, false); } Add(TablePoint(job), service.wipeSec, false); break; case StaffJobKind.Rescue: // 動けなくなったロボットの手前 0.8 m まで歩いて行き、rescue_s のあいだ動かす。 // そのあいだ、この台についてはよけない(よけると近づけない) _jobText = "立ち往生を動かす"; SetIgnoredRobot(job.robot); Add(RescueStandPoint(job.robot), service.rescueSec, false); break; } if (_steps.Count == 0) { Done(); return; } Go(_steps[0].point); } // ---------------- 立ち往生を動かしに行く(v5 3章)---------------- /// /// 動けなくなったロボットの **手前** に立つ点(Rescue の行き先)。 /// /// ロボットの位置そのものを行き先にすると、着けません。 /// 人がロボットをよける処理が、ロボットの中心から /// 人の半径 0.2 + ロボットの半径 0.25 + robotKeepClearM 0.3 = 0.75 m /// まで押しのけるのに、着いたとみなすのは 0.35 m(stoppingDistance + 0.05)だからです。 /// そこで、ロボットから **自分がいる側へ 0.8 m** 離れた点を行き先にします。 /// この台についてはよけないので(ignoreRobot)、押しのけずにそこまで行けます。 /// Vector3 RescueStandPoint(ServingRobotController robot) { if (robot == null) return transform.position; Vector3 r = robot.transform.position; r.y = 0f; Vector3 me = transform.position; me.y = 0f; Vector3 d = me - r; if (d.sqrMagnitude < 0.0001f) d = transform.forward; // 重なっているときは向いている方 d.y = 0f; if (d.sqrMagnitude < 0.0001f) d = Vector3.forward; return r + d.normalized * RescueStandM; } /// ロボットの中心からこれだけ手前に立って動かす(m) const float RescueStandM = 0.8f; /// /// 「この台はよけない」を入れ替える(v5 3章)。 /// 立ち往生の仕事を始めたらその台、やめたり終わったら null(ほかの台はいつもよける)。 /// void SetIgnoredRobot(ServingRobotController robot) { if (_yield == null) _yield = GetComponent(); if (_yield != null) _yield.ignoreRobot = robot; } /// 行き先を引き直す目安(m)。これ以上ずれたら引き直す const float RescueRetargetM = 0.25f; /// いまの行き先が「立ち往生を動かす」か bool IsRescueStep() { return _job != null && _job.kind == StaffJobKind.Rescue && _job.robot != null && _step >= 0 && _step < _steps.Count; } /// /// 立ち往生の行き先を、相手の位置に合わせて追いかける。 /// 戻り値 true なら、その点に着いた(このあと rescue_s のあいだ動かす)。 /// bool UpdateRescueStep() { Vector3 want = RescueStandPoint(_job.robot); // 相手が動いたら行き先を引き直す(引き直しすぎると経路が落ち着かないので目安を置く) Step s = _steps[_step]; if ((Flat(s.point) - Flat(want)).magnitude > RescueRetargetM) { s.point = want; _steps[_step] = s; _pending = want; _needsDestination = !PeopleNav.Go(_agent, want); } // 着いたかどうかは、その点との距離で見る(経路の残りは引き直しの直後に当てにならない) float gap = (Flat(transform.position) - Flat(_steps[_step].point)).magnitude; return gap <= _agent.stoppingDistance + 0.05f; } static Vector3 Flat(Vector3 v) { v.y = 0f; return v; } Vector3 TablePoint(StaffJob job) { if (ops == null || job.tableIndex < 0 || job.tableIndex >= ops.Tables.Count) return _targets.foodPass; return ops.Tables[job.tableIndex].deliveryPoint; } void Add(Vector3 point, float dwellSec, bool handDish) { Step s = new Step(); s.point = point; s.dwellSec = dwellSec; s.handDish = handDish; _steps.Add(s); } bool _wasInRegisterBack; /// /// 会計でないスタッフが店員側(レジの内側)に入っていないかを見る。 /// 店員側は1人ぶんなので、会計の仕事を持っていないスタッフは入らない。 /// void WatchRegisterBack() { if (ops == null || ops.store == null) { _wasInRegisterBack = false; return; } PlanRect back = ops.store.RegisterBackRect; bool paying = _job != null && _job.kind == StaffJobKind.Pay; bool inside = !back.IsEmpty && back.Contains(transform.position); if (inside && !paying) { ops.AddRegisterBackIntruder(Time.deltaTime, !_wasInRegisterBack); _wasInRegisterBack = true; return; } _wasInRegisterBack = inside && !paying; } /// /// 会計を済ませたとき、店員側(レジの内側)に立っていたかを残す。 /// 0 にならなければ、スタッフが客側に立ってしまっている。 /// void RecordPayPlace() { if (ops == null || ops.store == null || _targets == null) return; PlanRect back = ops.store.RegisterBackRect; bool atBack = !back.IsEmpty && back.Expand(0.1f).Contains(transform.position); // 立つ点と、実際に立った位置の距離 Vector3 a = transform.position; a.y = 0f; Vector3 b = _targets.registerBack; b.y = 0f; ops.AddRegisterPay(atBack, Vector3.Distance(a, b)); } void Deliver(Step s) { // 会計を済ませた瞬間に、店員側に立っていたかを残す if (_job != null && _job.kind == StaffJobKind.Pay) RecordPayPlace(); // rescue_s のあいだ動かし終わった。ロボットは止まっていた所から走りを続ける。 // robot が null なのは、待っているあいだにその往復が打ち切られたとき(何もしない) if (_job != null && _job.kind == StaffJobKind.Rescue) { if (_job.robot != null) _job.robot.Rescued(); return; } if (!s.handDish || _job == null || _job.dish == null) return; ops.DishArrived(_job.dish, false); } void Done() { StaffJob job = _job; _job = null; _steps.Clear(); _step = 0; _stepS = 0f; _jobText = "待機"; SetIgnoredRobot(null); // よけない台を元に戻す(v5 3章) if (job != null) { job.tries = 0; ops.FinishJob(job); } } void Go(Vector3 point) { _pending = point; _needsDestination = true; _stepS = 0f; if (PeopleNav.EnsureReady(transform, _agent) && PeopleNav.Go(_agent, point)) _needsDestination = false; } // ---------------- あきらめて、やり直す(B1)---------------- /// /// 行き先に着けないまま spec.staffGiveUpSec が過ぎたとき。 /// ・まだ staffGiveUpMaxTries 回未満なら、仕事を板に戻して別の仕事を取る /// ・その回数に達したら、その場所へ移して仕事を続ける(店が通れないしるし) /// bool GiveUpIfLate() { if (spec.staffGiveUpSec <= 0f || _stepS < spec.staffGiveUpSec) return false; StaffJob job = _job; Vector3 to = _step >= 0 && _step < _steps.Count ? _steps[_step].point : transform.position; // あきらめた回数は **仕事ごと** に数える(v5 3章)。 // 人ごとに数えていたときは、仕事が人から人へ回ると誰も上限に届かず、 // 「3回で移す」逃がし弁が働かないまま回り続けた int tries = ++job.tries; int max = Mathf.Max(1, spec.staffGiveUpMaxTries); if (tries >= max) { // 何度やっても着けない。その場所へ移して仕事を済ませる ops.AddGiveUp("スタッフ", _jobText, job != null ? job.tableId : 0, to, transform.position, _stepS, "移した"); job.tries = 0; PlaceAt(to); _stepS = 0f; return false; // 移したので、このコマの続きで着いた扱いにする } // 仕事を板に戻して、別の仕事を取る ops.AddGiveUp("スタッフ", _jobText, job != null ? job.tableId : 0, to, transform.position, _stepS, "あきらめた"); _job = null; _steps.Clear(); _step = 0; _dwelling = false; _stepS = 0f; _jobText = "待機"; SetIgnoredRobot(null); // よけない台を元に戻す(v5 3章) ops.AbandonJob(job); return true; } /// 体をその場所に移す(どうしても着けないとき) void PlaceAt(Vector3 p) { transform.position = p; if (Application.isPlaying && _agent != null && _agent.enabled && _agent.isOnNavMesh) _agent.Warp(p); } } }