Files
zeling_v2/Assets/_Game/Scripts/Enemies/EnemyMovement.cs
Joywayer a1b4e629aa feat: Implement Room Streaming System
- Add RoomStreamingManager to manage room loading and unloading based on player proximity.
- Create StreamingBudgetConfigSO for memory and performance budgeting of the streaming system.
- Introduce TransitionDirector to handle seamless and atmospheric fade transitions between rooms.
- Develop WorldGraph to represent room connectivity and facilitate neighbor queries and distance calculations.
- Implement RoomNode and RoomEdge classes to structure room data and connections.
2026-05-23 19:10:29 +08:00

262 lines
10 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 System;
using System.Collections;
using UnityEngine;
namespace BaseGames.Enemies
{
/// <summary>
/// 敌人移动组件(架构 07_EnemyModule §3
/// 实现:水平移动、面向目标、击退,以及导航连接段穿越(<see cref="INavLinkHandler"/>)。
///
/// 作为 <see cref="INavLinkHandler"/> 处理 Jump / Fall 两种 NavLink 类型:
/// - 跳跃连接Jump调用 <see cref="JumpToTarget"/> 施加物理冲量,等待落地后通知完成
/// - 下落连接Fall 水平对准目标X让重力自然下坠到达目标Y附近通知完成
/// 没有 EnemyMovement 组件(或 Jump 能力被移除)的敌人将无法通过跳跃连接,
/// 路径代价保持 TransformBasedMovement 兜底(仍可跳,但无自定义动画/物理)。
///
/// ⚠️ 使用 Rigidbody2D.velocityUnity 2022 LTS
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class EnemyMovement : MonoBehaviour, INavLinkHandler
{
[SerializeField] private EnemyStatsSO _config;
[SerializeField] private SpriteRenderer _spriteRenderer;
[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;
public bool IsGrounded { get; private set; }
/// <summary>当前朝向1 = 右,-1 = 左。</summary>
public int FacingDirection => _facingDir;
// ── 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)
{
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; }
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<Rigidbody2D>();
}
private void FixedUpdate()
{
IsGrounded = IsGroundedCheck();
}
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。</summary>
public void MoveHorizontal(float dir)
{
var vel = _rb.velocity;
vel.x = dir * _config.WalkSpeed;
_rb.velocity = vel;
UpdateFacing(dir);
}
/// <summary>显式指定速度BD 追击任务调用)。</summary>
public void MoveWithSpeed(float dir, float speed)
{
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);
}
public void ApplyKnockback(Vector2 dir, float force)
{
_rb.velocity = dir.normalized * force;
}
/// <summary>
/// 击飞冲量:向上 + 沿受击反方向水平。
/// sourceDir 为伤害来源朝向(通常是 DamageInfo.KnockbackDirection横向取其反方向。
/// </summary>
/// <param name="sourceDir">来袭方向(已归一化)</param>
/// <param name="horzForce">水平冲量大小</param>
/// <param name="upForce">纵向冲量大小</param>
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;
}
/// <summary>
/// 向目标位置抖跃(抛物线累加填充)。
/// 计算初速使尔子到达目标,用 Impulse 施加力。
/// </summary>
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;
int newDir = dir > 0f ? 1 : -1;
if (newDir == _facingDir) return;
_facingDir = newDir;
if (_spriteRenderer != null)
{
_spriteRenderer.flipX = newDir < 0;
}
else
{
// SpriteRenderer 未绑定时通过 localScale 翻转朝向
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<Collider2D>())
{
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));
#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);
}
}
}