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.
This commit is contained in:
2026-05-23 19:10:29 +08:00
parent 81c326af53
commit a1b4e629aa
165 changed files with 7904 additions and 313 deletions

View File

@@ -1,73 +1,225 @@
using UnityEngine;
using PathBerserker2d;
using BaseGames.Enemies;
using System;
using System.Collections.Generic;
namespace BaseGames.Enemies.Navigation
{
/// <summary>
/// PathBerserker2d 导航代理包装器(架构 07_EnemyModule §5
/// 实现 IPathAgent 接口,使 EnemyBase 和 BD Task 无需直接依赖 PB2d 类型。
/// PB2d APIUpdatePath(Vector2)、Stop()、TransformBasedMovement.movementSpeed、IsFollowingAPath。
/// PathBerserker2d 导航代理包装器(架构 07_EnemyModule §5 能力驱动版)。
///
/// 设计原则:
/// <list type="bullet">
/// <item>连接段穿越(跳跃/攀爬/传送等)委托给 <see cref="INavLinkHandler"/> 能力组件执行</item>
/// <item>能力自行驱动动画 + 物理,本组件只做"调度 → 等回调 → 通知 PB2d 继续"</item>
/// <item>无对应能力时自动回退到 TransformBasedMovement兜底行为</item>
/// <item>Awake 自动发现 GameObject 上的全部 INavLinkHandler并禁用 TBM 对应 FeatureFlag</item>
/// </list>
/// </summary>
[RequireComponent(typeof(NavAgent))]
[RequireComponent(typeof(TransformBasedMovement))]
public class EnemyNavAgent : MonoBehaviour, IPathAgent
{
private NavAgent _navAgent;
// ── 序列化 ──────────────────────────────────────────────────────
[Tooltip("边缘检测前向距离m")]
[SerializeField] private float _edgeCheckFwdOffset = 0.3f;
[Tooltip("边缘检测向下射线长度m")]
[SerializeField] private float _edgeCheckDownLen = 0.6f;
[Tooltip("边缘检测 LayerMask留空 = 所有层)")]
[SerializeField] private LayerMask _groundMask = ~0;
// ── IPathAgent 公开状态 ────────────────────────────────────────
public bool IsMoving => _navAgent != null && _navAgent.IsFollowingAPath;
public bool IsOnLink => _navAgent != null && _navAgent.IsOnLink;
public NavLinkType CurrentLinkType => _currentLinkType;
public Vector2 CurrentLinkStart => _currentLinkStart;
public Vector2 CurrentLinkEnd => _currentLinkEnd;
// ── IPathAgent 事件 ────────────────────────────────────────────
public event Action<NavLinkType> OnLinkStarted;
public event Action<NavLinkType> OnLinkCompleted;
public event Action OnNavPathFailed;
public event Action OnGoalReached;
// ── 私有 ────────────────────────────────────────────────────────
private NavAgent _navAgent;
private TransformBasedMovement _movement;
/// <summary>正在沿路径移动时为 true。</summary>
public bool IsMoving => _navAgent != null && _navAgent.IsFollowingAPath;
// 能力 handler 注册表NavLinkType → INavLinkHandler
private readonly Dictionary<NavLinkType, INavLinkHandler> _handlers
= new Dictionary<NavLinkType, INavLinkHandler>();
private INavLinkHandler _activeHandler;
public event System.Action OnNavPathFailed;
// 连接段状态缓存
private NavLinkType _currentLinkType = NavLinkType.None;
private Vector2 _currentLinkStart;
private Vector2 _currentLinkEnd;
// ── 初始化 ─────────────────────────────────────────────────────
private void Awake()
{
_navAgent = GetComponent<NavAgent>();
_movement = GetComponent<TransformBasedMovement>();
_navAgent.OnFailedToFindPath += HandlePathFailed;
// 自动发现 INavLinkHandler 组件并注册(包含子对象)
foreach (var handler in GetComponentsInChildren<INavLinkHandler>(true))
RegisterLinkHandler(handler);
_navAgent.OnStartLinkTraversal += HandleLinkStart;
_navAgent.OnSegmentTraversal += HandleSegmentTraversal;
_navAgent.OnFailedToFindPath += HandlePathFailed;
_navAgent.OnReachedGoal += HandleGoalReached;
}
private void OnDestroy()
{
if (_navAgent != null)
_navAgent.OnFailedToFindPath -= HandlePathFailed;
if (_navAgent == null) return;
_navAgent.OnStartLinkTraversal -= HandleLinkStart;
_navAgent.OnSegmentTraversal -= HandleSegmentTraversal;
_navAgent.OnFailedToFindPath -= HandlePathFailed;
_navAgent.OnReachedGoal -= HandleGoalReached;
}
private void HandlePathFailed(NavAgent _) => OnNavPathFailed?.Invoke();
public void RequestMoveTo(Vector2 target)
// ── Handler 注册 ───────────────────────────────────────────────
/// <summary>
/// 注册能力为某连接段类型的处理器。
/// 注册后 TransformBasedMovement 中对应 FeatureFlag 将被禁用,由能力全权负责该类型穿越。
/// </summary>
public void RegisterLinkHandler(INavLinkHandler handler)
{
_navAgent?.UpdatePath(target);
if (handler == null) return;
foreach (var type in handler.HandledLinkTypes)
{
_handlers[type] = handler;
// 禁用 TBM 对该类型的处理,完全交给能力
var flag = TypeToFeatureFlag(type);
if (flag != 0 && _movement != null)
_movement.enabledFeatures &= ~flag;
}
}
/// <summary>移除某处理器例如能力被永久禁用时。TBM FeatureFlag 自动恢复。</summary>
public void UnregisterLinkHandler(INavLinkHandler handler)
{
if (handler == null) return;
foreach (var type in handler.HandledLinkTypes)
{
if (_handlers.TryGetValue(type, out var registered) && ReferenceEquals(registered, handler))
{
_handlers.Remove(type);
// 恢复 TBM 对该类型的处理
var flag = TypeToFeatureFlag(type);
if (flag != 0 && _movement != null)
_movement.enabledFeatures |= flag;
}
}
}
// ── IPathAgent ─────────────────────────────────────────────────
public void RequestMoveTo(Vector2 target) => _navAgent?.UpdatePath(target);
public void StopNavigation()
{
_activeHandler?.AbortLinkTraversal();
_activeHandler = null;
_navAgent?.Stop();
}
public bool IsAtDestination()
{
if (_navAgent == null) return true;
// 已停止 OR 在目标线段上且不再跟随路径
return _navAgent.IsIdle;
}
public bool IsAtDestination() => _navAgent == null || _navAgent.IsIdle;
public void SetSpeed(float speed)
{
if (_movement != null) _movement.movementSpeed = speed;
}
public bool CanReach(Vector2 target) => _navAgent?.CanReach(target) ?? false;
public bool WalkToRandom() => _navAgent?.SetRandomDestination() ?? false;
public bool IsNearEdge()
{
// 双射线检测:脚下前方是否有地面
if (_navAgent == null) return false;
var origin = (Vector2)transform.position;
var facing = transform.localScale.x >= 0f ? Vector2.right : Vector2.left;
var groundMask = ~0; // 检测所有层;可收窄至 Ground 层
bool groundAhead = Physics2D.Raycast(origin + facing * 0.3f, Vector2.down, 0.5f, groundMask);
return !groundAhead;
return !Physics2D.Raycast(origin + facing * _edgeCheckFwdOffset,
Vector2.down, _edgeCheckDownLen, _groundMask);
}
// ── 底层 NavAgent 直接访问 ──────────────────────────────────────
/// <summary>原始 PB2d NavAgent供需要 PB2d 高级功能的能力直接使用。</summary>
public NavAgent RawNavAgent => _navAgent;
// ── 连接段调度核心 ─────────────────────────────────────────────
private void HandleLinkStart(NavAgent agent)
{
var seg = agent.CurrentPathSegment;
_currentLinkType = ParseLinkType(seg?.link?.LinkTypeName);
_currentLinkStart = seg?.LinkStart ?? Vector2.zero;
_currentLinkEnd = seg?.LinkEnd ?? Vector2.zero;
OnLinkStarted?.Invoke(_currentLinkType);
if (_handlers.TryGetValue(_currentLinkType, out var handler))
{
if (handler.CanHandleLink(_currentLinkType, _currentLinkStart, _currentLinkEnd))
{
_activeHandler = handler;
handler.BeginLinkTraversal(_currentLinkType, _currentLinkStart, _currentLinkEnd,
onComplete: () =>
{
_activeHandler = null;
_navAgent?.CompleteLinkTraversal();
});
return;
}
// CanHandleLink 返回 false 且 TBM 已禁用 → NavAgent 会卡住,警告设计者
Debug.LogWarning($"[EnemyNavAgent] '{handler.GetType().Name}' reported CanHandleLink=false " +
$"for {_currentLinkType} (TBM disabled). NavAgent may stall. " +
$"Check link distance/height vs. ability parameters.", this);
}
// 无 handler → TBM 兜底(其 FeatureFlag 仍开启)
}
private void HandleSegmentTraversal(NavAgent agent)
{
if (_currentLinkType != NavLinkType.None && !agent.IsOnLink)
{
var finished = _currentLinkType;
_currentLinkType = NavLinkType.None;
_currentLinkStart = Vector2.zero;
_currentLinkEnd = Vector2.zero;
OnLinkCompleted?.Invoke(finished);
}
}
private void HandlePathFailed(NavAgent _) => OnNavPathFailed?.Invoke();
private void HandleGoalReached(NavAgent _) => OnGoalReached?.Invoke();
// ── 工具 ───────────────────────────────────────────────────────
private static NavLinkType ParseLinkType(string name) => name switch
{
"corner" => NavLinkType.Corner,
"jump" => NavLinkType.Jump,
"fall" => NavLinkType.Fall,
"teleport" => NavLinkType.Teleport,
"climb" => NavLinkType.Climb,
"elevator" => NavLinkType.Elevator,
null or "" => NavLinkType.Segment,
_ => NavLinkType.Custom
};
/// <summary>NavLinkType → TransformBasedMovement.FeatureFlags 映射。</summary>
private static TransformBasedMovement.FeatureFlags TypeToFeatureFlag(NavLinkType type) => type switch
{
NavLinkType.Jump => TransformBasedMovement.FeatureFlags.JumpLinks,
NavLinkType.Fall => TransformBasedMovement.FeatureFlags.FallLinks,
NavLinkType.Corner => TransformBasedMovement.FeatureFlags.CornerLinks,
NavLinkType.Climb => TransformBasedMovement.FeatureFlags.ClimbLinks,
NavLinkType.Elevator => TransformBasedMovement.FeatureFlags.ElevatorLinks,
NavLinkType.Teleport => TransformBasedMovement.FeatureFlags.TeleportLinks,
_ => (TransformBasedMovement.FeatureFlags)0
};
}
}

View File

@@ -0,0 +1,165 @@
using System;
using UnityEngine;
namespace BaseGames.Enemies.Navigation
{
/// <summary>
/// 飞行单位直线导航代理。
/// 不依赖 PathBerserker2d 寻路,直接通过 Rigidbody2D.MovePosition 向目标点直飞。
///
/// 特性:
/// <list type="bullet">
/// <item>空闲时施加正弦波形悬停偏移,产生飘浮感。</item>
/// <item>NavLink 相关成员均返回安全默认值(飞行单位不使用平台连接段)。</item>
/// <item>目标到达后触发 <see cref="OnGoalReached"/> 事件(替代轮询 IsAtDestination。</item>
/// </list>
///
/// 使用方式:挂载到飞行怪 Prefab 根节点,替代 EnemyNavAgent
/// <see cref="EnemyBase.Awake"/> 的 GetComponent&lt;IPathAgent&gt;() 自动发现此组件。
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public sealed class FlyingDirectNavigator : MonoBehaviour, IPathAgent
{
[Header("移动参数")]
[Tooltip("直飞速度m/s")]
[SerializeField] private float _moveSpeed = 3f;
[Tooltip("到达目标的判定距离m")]
[SerializeField] private float _stoppingDist = 0.15f;
[Header("游荡参数WalkToRandom")]
[Tooltip("随机游荡目标点距当前位置的最大半径m。")]
[Min(0.1f)]
[SerializeField] private float _randomWalkRadius = 3f;
[Header("悬停参数(空闲 / 无目标时)")]
[Tooltip("悬停横向摆动速度m/s")]
[SerializeField] private float _hoverLateralSpeed = 0.5f;
[Tooltip("悬停纵向正弦频率Hz")]
[SerializeField] private float _hoverSineFrequency = 1.2f;
[Tooltip("悬停纵向正弦振幅m/s")]
[SerializeField] private float _hoverSineAmplitude = 0.25f;
[Tooltip("横向方向翻转周期s")]
[SerializeField] private float _hoverFlipInterval = 1.5f;
// ── IPathAgent 事件 ────────────────────────────────────────────
public event Action<NavLinkType> OnLinkStarted { add { } remove { } }
public event Action<NavLinkType> OnLinkCompleted { add { } remove { } }
public event Action OnNavPathFailed { add { } remove { } }
public event Action OnGoalReached;
// ── 状态 ───────────────────────────────────────────────────────
private Rigidbody2D _rb;
private Vector2? _destination;
private bool _isMoving;
private bool _goalFired;
private float _hoverTimer;
private float _hoverFlipTimer;
private float _hoverDir = 1f;
// ── IPathAgent 属性 ────────────────────────────────────────────
public bool IsMoving => _isMoving;
public bool IsOnLink => false;
public NavLinkType CurrentLinkType => NavLinkType.None;
public Vector2 CurrentLinkStart => Vector2.zero;
public Vector2 CurrentLinkEnd => Vector2.zero;
// ── Unity 生命周期 ─────────────────────────────────────────────
private void Awake()
{
_rb = GetComponent<Rigidbody2D>();
_rb.gravityScale = 0f;
_rb.constraints = RigidbodyConstraints2D.FreezeRotation;
}
private void FixedUpdate()
{
if (_destination.HasValue)
UpdateChase();
else
UpdateHover();
}
// ── IPathAgent 方法 ────────────────────────────────────────────
public void RequestMoveTo(Vector2 target)
{
_destination = target;
_isMoving = true;
_goalFired = false;
}
public void StopNavigation()
{
_destination = null;
_isMoving = false;
_rb.velocity = Vector2.zero;
}
public bool IsAtDestination()
{
if (!_destination.HasValue) return true;
return ((Vector2)transform.position - _destination.Value).sqrMagnitude
<= _stoppingDist * _stoppingDist;
}
public void SetSpeed(float speed) => _moveSpeed = speed;
public bool IsNearEdge() => false; // 飞行单位不检测平台边缘
public bool CanReach(Vector2 target) => true; // 飞行单位直飞,始终可达
public bool WalkToRandom()
{
// 在当前位置随机偏移一个 2D 方向(供 BD_WalkRandom 调用)
Vector2 offset = UnityEngine.Random.insideUnitCircle.normalized * _randomWalkRadius;
RequestMoveTo((Vector2)transform.position + offset);
return true;
}
// ── 内部移动逻辑 ───────────────────────────────────────────────
private void UpdateChase()
{
Vector2 myPos = _rb.position;
Vector2 target = _destination.Value;
float sqrDist = (target - myPos).sqrMagnitude;
if (sqrDist <= _stoppingDist * _stoppingDist)
{
_rb.velocity = Vector2.zero;
_isMoving = false;
_destination = null;
if (!_goalFired)
{
_goalFired = true;
OnGoalReached?.Invoke();
}
return;
}
Vector2 newPos = Vector2.MoveTowards(myPos, target, _moveSpeed * Time.fixedDeltaTime);
_rb.MovePosition(newPos);
// 面向移动方向
float dx = target.x - myPos.x;
if (Mathf.Abs(dx) > 0.05f)
{
var s = transform.localScale;
s.x = Mathf.Abs(s.x) * Mathf.Sign(dx);
transform.localScale = s;
}
}
private void UpdateHover()
{
_hoverFlipTimer += Time.fixedDeltaTime;
if (_hoverFlipTimer >= _hoverFlipInterval)
{
_hoverFlipTimer = 0f;
_hoverDir = -_hoverDir;
}
float sineY = Mathf.Sin(Time.time * _hoverSineFrequency * Mathf.PI * 2f) * _hoverSineAmplitude;
_rb.velocity = new Vector2(_hoverDir * _hoverLateralSpeed, sineY);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 17b018161daf99846969142e6184b858
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: