using System; using System.Collections; using Animancer; using UnityEngine; namespace BaseGames.Enemies { /// /// 敌人移动组件(架构 07_EnemyModule §3)。 /// 实现:水平移动、面向目标、击退,以及导航连接段穿越()。 /// /// 作为 处理 Jump / Fall 两种 NavLink 类型: /// - 跳跃连接(Jump):调用 施加物理冲量,等待落地后通知完成 /// - 下落连接(Fall) :水平对准目标X,让重力自然下坠,到达目标Y附近通知完成 /// 没有 EnemyMovement 组件(或 Jump 能力被移除)的敌人将无法通过跳跃连接, /// 路径代价保持 TransformBasedMovement 兜底(仍可跳,但无自定义动画/物理)。 /// /// ⚠️ 使用 Rigidbody2D.velocity(Unity 2022 LTS)。 /// [RequireComponent(typeof(Rigidbody2D))] public class EnemyMovement : MonoBehaviour, INavLinkHandler { [SerializeField] private EnemyStatsSO _config; [SerializeField] private SpriteRenderer _spriteRenderer; [Header("转身动画")] [Tooltip("开启后,敌人翻转方向时播放转身动画并暂停水平移动,动画结束后完成翻转")] [SerializeField] private bool _enableTurnAnimation = false; [Tooltip("Animancer 组件引用;留空则在 Awake 时自动从父级查找")] [SerializeField] private AnimancerComponent _animancer; [Tooltip("动画配置 SO;留空则在 Awake 时自动从 EnemyBase 读取")] [SerializeField] private EnemyAnimationConfigSO _animConfig; [Header("导航跳跃能力(INavLinkHandler)")] [Tooltip("可处理的最大跳跃垂直高度(超出则让 TBM 兜底)")] [SerializeField] private float _navJumpMaxHeight = 6f; [Tooltip("可处理的最大跳跃水平距离")] [SerializeField] private float _navJumpMaxDist = 10f; [Tooltip("地面检测射线长度(用于判断跳跃是否落地)")] [SerializeField] private float _groundCheckDist = 0.35f; [Tooltip("地面层 LayerMask")] [SerializeField] private LayerMask _groundMask; private Rigidbody2D _rb; private int _facingDir = 1; private Coroutine _linkCoroutine; // ── 转身状态 ──────────────────────────────────────────────────────── private bool _isTurning; private int _pendingFacingDir; // 转身目标方向,转身完成后 ApplyFacingFlip 使用 private Coroutine _turnCoroutine; // ── 输入信号(BD 任务在 Update 写入,FixedUpdate 消费后自动清零)── public EnemyMoveInput PendingInput; public bool IsGrounded { get; private set; } /// 当前朝向:1 = 右,-1 = 左。 public int FacingDirection => _facingDir; /// 当前是否正在播放转身动画(移动输入在此期间被屏蔽)。 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 }; public NavLinkType[] HandledLinkTypes => _handledTypes; public bool CanHandleLink(NavLinkType type, Vector2 linkStart, Vector2 linkEnd) { if (type == NavLinkType.Jump) { float dy = Mathf.Abs(linkEnd.y - linkStart.y); float dx = Mathf.Abs(linkEnd.x - linkStart.x); return dy <= _navJumpMaxHeight && dx <= _navJumpMaxDist; } return true; // Fall 总是可以处理 } public void BeginLinkTraversal(NavLinkType type, Vector2 linkStart, Vector2 linkEnd, Action onComplete) { CancelTurn(); // 进入连接段前中止任何进行中的转身 if (_linkCoroutine != null) StopCoroutine(_linkCoroutine); _linkCoroutine = type == NavLinkType.Jump ? StartCoroutine(JumpLinkCoroutine(linkStart, linkEnd, onComplete)) : StartCoroutine(FallLinkCoroutine(linkStart, linkEnd, onComplete)); } public void AbortLinkTraversal() { if (_linkCoroutine != null) { StopCoroutine(_linkCoroutine); _linkCoroutine = null; } CancelTurn(); StopHorizontal(); } private IEnumerator JumpLinkCoroutine(Vector2 start, Vector2 end, Action onComplete) { JumpToTarget(end); yield return null; // 等一帧让 velocity 生效 // 等待离地后落地(超时 3s 防死锁) float timer = 0f; bool leftGround = false; while (timer < 3f) { timer += Time.fixedDeltaTime; yield return new WaitForFixedUpdate(); if (!leftGround && !IsGroundedCheck()) { leftGround = true; } if (leftGround && IsGroundedCheck()) break; } StopHorizontal(); _linkCoroutine = null; onComplete?.Invoke(); } private IEnumerator FallLinkCoroutine(Vector2 start, Vector2 end, Action onComplete) { // 水平对准目标 float dx = end.x - (float)transform.position.x; if (Mathf.Abs(dx) > 0.15f) MoveHorizontal(Mathf.Sign(dx)); // 等待接近目标Y(重力驱动下落) float timer = 0f; while (timer < 3f) { timer += Time.fixedDeltaTime; yield return new WaitForFixedUpdate(); if (IsGroundedCheck() && Mathf.Abs(_rb.position.y - end.y) < 0.6f) break; } StopHorizontal(); _linkCoroutine = null; onComplete?.Invoke(); } private bool IsGroundedCheck() => Physics2D.Raycast(_rb.position, Vector2.down, _groundCheckDist, _groundMask); private void Awake() { Debug.Assert(_config != null, "[EnemyMovement] _config 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this); _rb = GetComponent(); // 从 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(true); if (_animConfig == null) { var enemyBase = GetComponentInParent(true); if (enemyBase != null) _animConfig = enemyBase.AnimConfig; } } } 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); } } /// 按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。转身动画期间调用无效。 public void MoveHorizontal(float dir) { if (_isTurning) return; var vel = _rb.velocity; vel.x = dir * _config.WalkSpeed; _rb.velocity = vel; UpdateFacing(dir); } /// 显式指定速度(BD 追击任务调用)。转身动画期间调用无效。 public void MoveWithSpeed(float dir, float speed) { if (_isTurning) return; var vel = _rb.velocity; vel.x = dir * speed; _rb.velocity = vel; UpdateFacing(dir); } /// 朝向指定世界坐标(通常传入玩家位置)。 public void FaceTarget(Vector2 targetPos) { float dir = targetPos.x < transform.position.x ? -1f : 1f; UpdateFacing(dir); } /// /// 直接指定朝向方向。dir: +1 = 右,-1 = 左。 /// 若启用转身动画且方向确实改变,会触发转身流程。 /// public void FaceDirection(int dir) { if (dir == 0) return; UpdateFacing(dir > 0 ? 1f : -1f); } /// 朝向右方(+X)。 public void FaceRight() => FaceDirection(1); /// 朝向左方(-X)。 public void FaceLeft() => FaceDirection(-1); public void ApplyKnockback(Vector2 dir, float force) { _rb.velocity = dir.normalized * force; } /// /// 击飞冲量:向上 + 沿受击反方向水平。 /// sourceDir 为伤害来源朝向(通常是 DamageInfo.KnockbackDirection),横向取其反方向。 /// /// 来袭方向(已归一化) /// 水平冲量大小 /// 纵向冲量大小 public void LaunchKnockup(Vector2 sourceDir, float horzForce, float upForce) { if (_rb == null) return; float horzSign = sourceDir.x >= 0f ? -1f : 1f; // 反方向弹飞 _rb.velocity = new Vector2(horzSign * horzForce, upForce); } public void StopHorizontal() { var vel = _rb.velocity; vel.x = 0f; _rb.velocity = vel; } /// /// 向目标位置抖跃(抛物线累加填充)。 /// 计算初速使尔子到达目标,用 Impulse 施加力。 /// public void JumpToTarget(Vector2 target) { if (_rb == null) return; Vector2 delta = target - (Vector2)transform.position; float gravMag = Mathf.Abs(Physics2D.gravity.y * _rb.gravityScale); float timeAloft = Mathf.Max(0.1f, delta.x != 0f ? Mathf.Abs(delta.x) / _config.RunSpeed : 0.5f); float vy = (delta.y - 0.5f * (-gravMag) * timeAloft * timeAloft) / timeAloft; float vx = delta.x / timeAloft; _rb.velocity = new Vector2(vx, vy); UpdateFacing(vx); } private void UpdateFacing(float dir) { if (Mathf.Approximately(dir, 0f)) return; if (_isTurning) return; // 转身进行中,忽略新的朝向请求 int newDir = dir > 0f ? 1 : -1; if (newDir == _facingDir) return; if (_enableTurnAnimation && _animancer != null && _animConfig?.Turn != null) { // 启动转身协程:动画播完后再实际翻转 _pendingFacingDir = newDir; if (_turnCoroutine != null) StopCoroutine(_turnCoroutine); _turnCoroutine = StartCoroutine(TurnCoroutine(newDir)); } else { ApplyFacingFlip(newDir); } } /// 转身动画协程:停止水平移动 → 播放 Turn 动画 → 翻转朝向 → 恢复。 private IEnumerator TurnCoroutine(int newDir) { _isTurning = true; StopHorizontal(); // yield return state:Animancer 的 AnimancerState 是 CustomYieldInstruction, // 等待动画自然播完,与 Layer/State 速度缩放无关,比手动计时更可靠。 var state = _animancer.Play(_animConfig.Turn); yield return state; ApplyFacingFlip(newDir); _isTurning = false; _turnCoroutine = null; } /// /// 立即中止进行中的转身协程,并将朝向应用到待转方向。 /// 受击、死亡、NavLink 穿越等外部中断时调用。 /// public void CancelTurn() { if (_turnCoroutine == null) return; StopCoroutine(_turnCoroutine); _turnCoroutine = null; if (_isTurning) { ApplyFacingFlip(_pendingFacingDir); _isTurning = false; } } /// 真正执行朝向翻转。始终用 localScale 翻转,子对象(传感器 RaySensor2D)随之正确翻转。 private void ApplyFacingFlip(int newDir) { _facingDir = newDir; // 若挂有 SpriteRenderer,重置 flipX = false(localScale 已负责镜像,避免双重翻转)。 if (_spriteRenderer != null) _spriteRenderer.flipX = false; Vector3 s = transform.localScale; transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z); } private void OnDrawGizmos() { #if UNITY_EDITOR // ── 1. 敌人物理轮廓(珊瑚红,区别于玩家绿色)──────────────── Gizmos.color = new Color(1f, 0.45f, 0.35f, 0.65f); foreach (var col in GetComponents()) { if (col.isTrigger) continue; BaseGames.Combat.HitBox.DrawCollider2DWire(col); } // ── 2. 朝向箭头(橙色)────────────────────────────────────── 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 } private void OnDrawGizmosSelected() { #if UNITY_EDITOR // 运行时:青色箭头显示速度向量(选中时) if (!Application.isPlaying || _rb == null) return; Vector2 vel = _rb.velocity; if (vel.sqrMagnitude < 0.01f) return; DrawArrow2D(transform.position, transform.position + (Vector3)(vel * 0.12f), new Color(0.2f, 0.9f, 1f, 0.9f), 0.1f); #endif } // 在 Gizmos 空间绘制带箭头的 2D 有向线段 private static void DrawArrow2D(Vector3 from, Vector3 to, Color color, float headLen = 0.15f) { Vector3 dir = to - from; if (dir.sqrMagnitude < 0.0001f) return; dir = dir.normalized; Gizmos.color = color; Gizmos.DrawLine(from, to); float cos = 0.8192f, sin = 0.5736f; // cos/sin 35° float bx = -dir.x, by = -dir.y; Vector3 wing1 = new Vector3(bx * cos - by * sin, bx * sin + by * cos, 0f) * headLen; Vector3 wing2 = new Vector3(bx * cos + by * sin, -bx * sin + by * cos, 0f) * headLen; Gizmos.DrawLine(to, to + wing1); Gizmos.DrawLine(to, to + wing2); } } }