feat(enemy): Wander 到达随机点后可配置停顿时长

到达一个段内随机游走点后原地停顿 [_wanderPauseMin, _wanderPauseMax] 秒再挑下一个点:
- 两个可配置字段(相等=固定时长;都为 0=不停顿立即挑点),每次停顿时长在区间内随机
- 停顿期间切 Idle 步态(否则站着播 Walk=原地踏步),速度归零
- 切换 locomotion 模式时清停顿态,避免残留
实测:移动→到点→停顿(Idle,velX=0)→再出发 循环正常。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-22 13:45:21 +08:00
co-authored by Claude Opus 4.8
parent 31b77fa473
commit 7f7d237915
@@ -14,6 +14,12 @@ namespace BaseGames.Enemies
[Header("巡逻策略(Wander/Pace/Waypoints")]
[SerializeField] private PatrolStrategy _patrolStrategy = PatrolStrategy.Wander;
[Header("Wander 策略参数")]
[Tooltip("到达一个随机游走点后的停顿时长下限(秒)。停顿期间原地待机(播 Idle),再挑下一个点。")]
[SerializeField] [Min(0f)] private float _wanderPauseMin = 0.5f;
[Tooltip("到达一个随机游走点后的停顿时长上限(秒)。与下限相同=固定时长;两者都为 0=不停顿、立即挑下一个点。")]
[SerializeField] [Min(0f)] private float _wanderPauseMax = 1.5f;
[Header("Waypoints 策略参数")]
[SerializeField] private Transform[] _waypoints;
[SerializeField] private bool _pingPong;
@@ -32,6 +38,9 @@ namespace BaseGames.Enemies
private int _wpIndex = -1;
private int _wpDir = 1;
private bool _warnedNoWaypoints;
// Wander 到点停顿状态
private bool _wanderPausing;
private float _wanderPauseTimer;
public LocomotionMode CurrentMode => _mode;
public bool IsMoving => _enemy != null && _enemy.Nav != null && _enemy.Nav.IsMoving;
@@ -48,6 +57,7 @@ namespace BaseGames.Enemies
{
if (_mode == mode) return;
_mode = mode;
_wanderPausing = false; // 切换模式清 Wander 停顿态
if (mode == LocomotionMode.Idle) _enemy?.StopMovement();
if (mode == LocomotionMode.Patrol && _enemy?.Stats != null)
_enemy.Nav?.SetSpeed(_enemy.Stats.WalkSpeed);
@@ -83,11 +93,16 @@ namespace BaseGames.Enemies
private void Update()
{
if (_enemy == null) return;
if (!_gaitInit || _gaitMode != _mode)
// Wander 到点停顿期间用 Idle 步态(原地待机,否则站着播 Walk=原地踏步)。
LocomotionMode gait = (_mode == LocomotionMode.Patrol
&& _patrolStrategy == PatrolStrategy.Wander
&& _wanderPausing)
? LocomotionMode.Idle : _mode;
if (!_gaitInit || _gaitMode != gait)
{
_enemy.PlayLocomotionClip(_mode);
_enemy.PlayLocomotionClip(gait);
// 招式动画锁定期内不算"已应用",解锁后重新同步步态(否则 Skill_End 后卡在收招动画)
if (!_enemy.IsAnimLocked) { _gaitMode = _mode; _gaitInit = true; }
if (!_enemy.IsAnimLocked) { _gaitMode = gait; _gaitInit = true; }
}
switch (_mode)
{
@@ -106,14 +121,38 @@ namespace BaseGames.Enemies
}
}
/// <summary>
/// 段内随机游走:随机点限制在当前所站的导航段上(不跨 NavLink,避免崖边夹停/卡 link),
/// 到达一个点后原地停顿 [_wanderPauseMin, _wanderPauseMax] 秒(期间播 Idle),再挑下一个点。
/// </summary>
private void TickWander()
{
if (_enemy.Nav == null) return;
// 移动中:清停顿态,等到达
if (_enemy.Nav.IsMoving) { _wanderPausing = false; return; }
// 已到达(或尚未出发):先停顿计时,再挑下一个点
if (!_wanderPausing)
{
_wanderPausing = true;
_wanderPauseTimer = Random.Range(_wanderPauseMin, Mathf.Max(_wanderPauseMin, _wanderPauseMax));
}
_wanderPauseTimer -= Time.deltaTime;
if (_wanderPauseTimer <= 0f)
{
_wanderPausing = false;
_enemy.Nav.WalkToRandomOnSegment();
}
}
private void TickPatrol()
{
switch (_patrolStrategy)
{
case PatrolStrategy.Wander:
// 段内随机游走:随机点限制在当前所站的导航段上,不跨越 NavLink,
// 避免地面单位游走时被崖边夹停或卡在过不去的跳/落 link 处。
if (_enemy.Nav != null && !_enemy.Nav.IsMoving) _enemy.Nav.WalkToRandomOnSegment();
TickWander();
break;
case PatrolStrategy.Pace: // 撞墙/悬崖翻向的来回踱步(速度驱动,不依赖 nav)