feat: Update enemy AI and movement systems

- Enhanced Physics2D layer collision report with new interactions between Player and Enemy layers.
- Refactored BD_InvestigateLastKnown to streamline animation handling and improve readability.
- Simplified BD_MaintainCombatDistance by consolidating movement stop logic.
- Updated BD_MoveToPlayer to set AI phase on start.
- Improved BD_Patrol logic with better handling of stuck states and path failures.
- Enhanced BD_PatrolWaypoints to manage stuck conditions and retry logic more effectively.
- Refined BD_ReturnToHome to remove unnecessary animation calls.
- Updated BD_WalkRandom to ensure AI phase is set correctly on start.
- Improved EnemyAbilityBase to delegate target facing to the movement system.
- Enhanced EnemyBase with new movement methods for better control.
- Refactored EnemyMovement to introduce a new input system for handling movement and facing.
- Added EnemyMoveInput struct to encapsulate movement intentions.
- Updated Physics2DSettings to reflect new layer collision matrix.
- Introduced RTK CLI instructions for optimized command usage.
This commit is contained in:
2026-05-29 17:01:59 +08:00
parent e24ecc9589
commit bcd8b0e90b
19 changed files with 179534 additions and 175 deletions

View File

@@ -50,12 +50,31 @@ namespace BaseGames.Enemies
private int _pendingFacingDir; // 转身目标方向,转身完成后 ApplyFacingFlip 使用
private Coroutine _turnCoroutine;
// ── 输入信号BD 任务在 Update 写入FixedUpdate 消费后自动清零)──
public EnemyMoveInput PendingInput;
public bool IsGrounded { get; private set; }
/// <summary>当前朝向1 = 右,-1 = 左。</summary>
public int FacingDirection => _facingDir;
/// <summary>当前是否正在播放转身动画(移动输入在此期间被屏蔽)。</summary>
public bool IsTurning => _isTurning;
#if UNITY_EDITOR
[Header("── 运行时调试(仅 Editor──")]
[SerializeField] private int _dbg_FacingDirection;
[SerializeField] private float _dbg_VelocityX;
[SerializeField] private float _dbg_VelocityY;
[SerializeField] private bool _dbg_IsGrounded;
[SerializeField] private bool _dbg_IsTurning;
[Header("── 输入信号(仅 Editor──")]
[SerializeField] private float _dbg_Input_MoveDir;
[SerializeField] private float _dbg_Input_MoveSpeed;
[SerializeField] private bool _dbg_Input_WantStop;
[SerializeField] private bool _dbg_Input_WantFace;
[SerializeField] private Vector2 _dbg_Input_FaceTargetPos;
[SerializeField] private int _dbg_Input_FaceDir;
#endif
// ── INavLinkHandler ────────────────────────────────────────────
private static readonly NavLinkType[] _handledTypes =
new[] { NavLinkType.Jump, NavLinkType.Fall };
@@ -136,6 +155,25 @@ namespace BaseGames.Enemies
Debug.Assert(_config != null, "[EnemyMovement] _config 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
_rb = GetComponent<Rigidbody2D>();
// 从 Sprite 或 localScale 的初始状态推断朝向,并统一切换为 localScale 翻转。
// 这样子对象(含 RaySensor2D会随 localScale 正确翻转,不再依赖 flipX。
if (_spriteRenderer != null)
{
// 两个信号均可能携带初始朝向信息flipX 或 localScale.x < 0
// XOR 组合:恰好一个翻转 → 面左;两个都翻(互相抵消)→ 面右。
bool flippedBySprite = _spriteRenderer.flipX;
bool flippedByScale = transform.localScale.x < 0f;
_facingDir = (flippedBySprite ^ flippedByScale) ? -1 : 1;
_spriteRenderer.flipX = false; // 后续由 localScale 驱动,避免双重镜像
Vector3 s = transform.localScale;
transform.localScale = new Vector3(Mathf.Abs(s.x) * _facingDir, s.y, s.z);
}
else
{
_facingDir = transform.localScale.x >= 0f ? 1 : -1;
}
if (_enableTurnAnimation)
{
if (_animancer == null) _animancer = GetComponentInParent<AnimancerComponent>(true);
@@ -147,9 +185,71 @@ namespace BaseGames.Enemies
}
}
private void OnDisable()
{
// 持久信号在对象禁用时必须清零,防止重新启用时继承残留移动状态。
PendingInput = default;
StopHorizontal();
}
private void FixedUpdate()
{
IsGrounded = IsGroundedCheck();
#if UNITY_EDITOR
_dbg_Input_MoveDir = PendingInput.MoveDir;
_dbg_Input_MoveSpeed = PendingInput.MoveSpeed;
_dbg_Input_WantStop = PendingInput.WantStop;
_dbg_Input_WantFace = PendingInput.WantFace;
_dbg_Input_FaceTargetPos = PendingInput.FaceTargetPos;
_dbg_Input_FaceDir = PendingInput.FaceDir;
#endif
ConsumeInput();
#if UNITY_EDITOR
_dbg_FacingDirection = _facingDir;
_dbg_VelocityX = _rb != null ? _rb.velocity.x : 0f;
_dbg_VelocityY = _rb != null ? _rb.velocity.y : 0f;
_dbg_IsGrounded = IsGrounded;
_dbg_IsTurning = _isTurning;
#endif
}
private void ConsumeInput()
{
// ── 一次性脉冲:消费后清零 ─────────────────────────────────────
// WantStop / WantFace 只需写一次,消费后自动清除,
// 避免 BD 任务每帧续写而产生的不必要开销。
bool wantStop = PendingInput.WantStop;
bool wantFace = PendingInput.WantFace;
int faceDir = PendingInput.FaceDir;
var facePosSnapshot = PendingInput.FaceTargetPos;
PendingInput.WantStop = false;
PendingInput.WantFace = false;
// ── 持久字段MoveDir / MoveSpeed 不清零 ─────────────────────
// 解决 FixedUpdate 频率 > Update 频率时的空帧问题:
// 两次 Update 之间如果 FixedUpdate 多执行一次,之前写入的 MoveDir
// 仍然有效,不会产生意外的 StopHorizontal。
if (wantStop)
{
PendingInput.MoveDir = 0f;
PendingInput.MoveSpeed = 0f;
StopHorizontal();
}
else if (PendingInput.MoveDir != 0f)
{
if (PendingInput.MoveSpeed > 0f)
MoveWithSpeed(PendingInput.MoveDir, PendingInput.MoveSpeed);
else
MoveHorizontal(PendingInput.MoveDir);
}
if (wantFace && !_isTurning)
{
if (faceDir != 0)
UpdateFacing(faceDir > 0 ? 1f : -1f);
else
FaceTarget(facePosSnapshot);
}
}
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。转身动画期间调用无效。</summary>
@@ -267,18 +367,13 @@ namespace BaseGames.Enemies
_isTurning = true;
StopHorizontal();
_animancer.Play(_animConfig.Turn);
float elapsed = 0f;
float duration = _animConfig.Turn.length;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
yield return null;
}
// yield return stateAnimancer 的 AnimancerState 是 CustomYieldInstruction
// 等待动画自然播完,与 Layer/State 速度缩放无关,比手动计时更可靠。
var state = _animancer.Play(_animConfig.Turn);
yield return state;
ApplyFacingFlip(newDir);
_isTurning = false;
_isTurning = false;
_turnCoroutine = null;
}
@@ -298,17 +393,15 @@ namespace BaseGames.Enemies
}
}
/// <summary>真正执行朝向翻转(修改 SpriteRenderer.flipX 或 localScale。</summary>
/// <summary>真正执行朝向翻转。始终用 localScale 翻转,子对象(传感器 RaySensor2D随之正确翻转。</summary>
private void ApplyFacingFlip(int newDir)
{
_facingDir = newDir;
// 若挂有 SpriteRenderer重置 flipX = falselocalScale 已负责镜像,避免双重翻转)。
if (_spriteRenderer != null)
_spriteRenderer.flipX = newDir < 0;
else
{
Vector3 s = transform.localScale;
transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z);
}
_spriteRenderer.flipX = false;
Vector3 s = transform.localScale;
transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z);
}
private void OnDrawGizmos()
@@ -326,6 +419,18 @@ namespace BaseGames.Enemies
Vector3 center = transform.position;
DrawArrow2D(center, center + new Vector3(_facingDir * 0.5f, 0f, 0f),
new Color(1f, 0.6f, 0.1f, 0.9f));
// ── 3. 地面检测射线(接地亮绿 / 未接地暗绿)─────────────────
if (_groundCheckDist > 0f)
{
bool grounded = Application.isPlaying && IsGrounded;
Gizmos.color = grounded
? new Color(0.2f, 1f, 0.35f, 0.90f)
: new Color(0.4f, 0.75f, 0.4f, 0.40f);
Vector3 origin = transform.position;
Gizmos.DrawLine(origin, origin + Vector3.down * _groundCheckDist);
Gizmos.DrawWireSphere(origin + Vector3.down * _groundCheckDist, 0.04f);
}
#endif
}