Files
zeling_v2/Assets/_Game/Scripts/Enemies/Navigation/EnemyLocomotion.cs
T
joywayer 6f52e4ff81 @
fix(enemy): Pace 巡逻改边沿触发翻向,修边缘左右抖动

TickPatrol 跑在 Update(可变帧率),IsWallAhead/IsLedgeAhead 在 FixedUpdate
刷新。原电平触发(每帧 blocked 就翻向)会在检测刷新前的多个 Update 帧把同一
障碍连翻多次→掉头边缘左右抖动几次才正常。改为边沿触发(仅障碍首次出现时翻
一次),用 _paceBlockedPrev 记录上帧状态。PlayMode 验证:踱步移动正常、无
连续快速翻转。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-07-10 14:36:53 +08:00

163 lines
6.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using UnityEngine;
using BaseGames.AI;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人移动执行器:唯一的移动/朝向入口。AI 状态与能力经 IEnemyLocomotion
/// 声明意图,本组件每帧把当前模式翻译为对 EnemyBase/IPathAgent 的调用。
/// 取代散落的 MoveTo/StopMovement/FacePlayer/FaceTarget 直调与三个协程能力。
/// </summary>
[DisallowMultipleComponent]
public sealed class EnemyLocomotion : MonoBehaviour, IEnemyLocomotion
{
[Header("巡逻策略(Wander/Pace/Waypoints")]
[SerializeField] private PatrolStrategy _patrolStrategy = PatrolStrategy.Wander;
[Header("Waypoints 策略参数")]
[SerializeField] private Transform[] _waypoints;
[SerializeField] private bool _pingPong;
[SerializeField] private float _waypointArriveRadius = 0.4f;
private EnemyBase _enemy;
private LocomotionMode _mode = LocomotionMode.Idle;
private Transform _approachTarget;
private Vector2 _facePoint;
private LocomotionMode _gaitMode;
private bool _gaitInit;
private float _paceDir = 1f;
private bool _paceBlockedPrev;
private int _wpIndex = -1;
private int _wpDir = 1;
private bool _warnedNoWaypoints;
public LocomotionMode CurrentMode => _mode;
public bool IsMoving => _enemy != null && _enemy.Nav != null && _enemy.Nav.IsMoving;
private void Awake()
{
_enemy = GetComponentInParent<EnemyBase>();
if (_enemy == null)
Debug.LogError("[EnemyLocomotion] 找不到 EnemyBase。", this);
}
// ── IEnemyLocomotion(声明意图,不立即 actuate 之外的副作用)──
public void SetMode(LocomotionMode mode)
{
if (_mode == mode) return;
_mode = mode;
if (mode == LocomotionMode.Idle) _enemy?.StopMovement();
if (mode == LocomotionMode.Patrol && _enemy?.Stats != null)
_enemy.Nav?.SetSpeed(_enemy.Stats.WalkSpeed);
}
public void Approach(Transform target)
{
_mode = LocomotionMode.Approach;
_approachTarget = target;
if (_enemy?.Stats != null) _enemy.Nav?.SetSpeed(_enemy.Stats.RunSpeed);
}
public void MoveTo(Vector2 point)
{
_mode = LocomotionMode.Approach;
_approachTarget = null;
_enemy?.MoveTo(point);
}
public void Face(Vector2 lookAt)
{
_mode = LocomotionMode.Face;
_facePoint = lookAt;
}
public void Stop()
{
_mode = LocomotionMode.Idle;
_enemy?.StopMovement();
}
// ── 每帧把模式翻译成执行 ──
private void Update()
{
if (_enemy == null) return;
if (!_gaitInit || _gaitMode != _mode)
{
_enemy.PlayLocomotionClip(_mode);
_gaitMode = _mode;
_gaitInit = true;
}
switch (_mode)
{
case LocomotionMode.Idle:
break; // SetMode(Idle) 已停;保持
case LocomotionMode.Patrol:
TickPatrol();
break;
case LocomotionMode.Face:
_enemy.StopMovement();
_enemy.FaceTarget(_facePoint);
break;
case LocomotionMode.Approach:
if (_approachTarget != null) _enemy.MoveTo(_approachTarget.position);
break;
}
}
private void TickPatrol()
{
switch (_patrolStrategy)
{
case PatrolStrategy.Wander:
if (_enemy.Nav != null && !_enemy.Nav.IsMoving) _enemy.Nav.WalkToRandom();
break;
case PatrolStrategy.Pace: // 撞墙/悬崖翻向的来回踱步(速度驱动,不依赖 nav)
var mv = _enemy.Movement;
bool blocked = mv != null && (mv.IsWallAhead || mv.IsLedgeAhead);
// 边沿触发:仅在"障碍首次出现"时翻向一次。TickPatrol 跑在 Update(可变帧率)
// 而 IsWallAhead/IsLedgeAhead 在 FixedUpdate 刷新——若用电平触发(每帧 blocked 就翻),
// 检测刷新前的多个 Update 帧会把同一障碍连翻多次→边缘左右抖动。
if (blocked && !_paceBlockedPrev) _paceDir = -_paceDir;
_paceBlockedPrev = blocked;
_enemy.MoveInDirection(_paceDir);
break;
case PatrolStrategy.Waypoints:
if (_waypoints == null || _waypoints.Length == 0)
{
if (!_warnedNoWaypoints)
{
Debug.LogWarning("[EnemyLocomotion] PatrolStrategy=Waypoints 但未配置 _waypoints,无法巡逻。", this);
_warnedNoWaypoints = true;
}
break;
}
// 仅首次或"已到达当前路点"时推进并寻路——不依赖 IsMoving
// PathBerserker2d 寻路跨帧异步,IsMoving 在寻路计算期间为 false,会误触发每帧推进)。
if (_wpIndex < 0 ||
Vector2.Distance(_enemy.transform.position, _waypoints[_wpIndex].position) <= _waypointArriveRadius)
{
AdvanceWaypoint();
_enemy.MoveTo(_waypoints[_wpIndex].position);
}
break;
}
}
private void AdvanceWaypoint()
{
if (_pingPong)
{
int next = _wpIndex + _wpDir;
if (next < 0 || next >= _waypoints.Length) { _wpDir = -_wpDir; next = _wpIndex + _wpDir; }
_wpIndex = Mathf.Clamp(next, 0, _waypoints.Length - 1);
}
else
{
_wpIndex = (_wpIndex + 1) % _waypoints.Length;
}
}
}
}