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