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

@@ -0,0 +1,184 @@
#if GRAPH_DESIGNER
using UnityEngine;
using Opsive.BehaviorDesigner.Runtime.Tasks;
using Opsive.BehaviorDesigner.Runtime.Tasks.Actions;
using BaseGames.Enemies;
namespace BaseGames.Enemies.AI
{
/// <summary>
/// BD Action多步搜查Navigate → LookAround → RandomWalk × N
///
/// <list type="bullet">
/// <item><b>Navigate</b>:导航至最后可见位置。</item>
/// <item><b>LookAround</b>:到达后原地环顾(播放 Look 动画,持续 LookAroundDuration。</item>
/// <item><b>WalkRandom</b>:向搜查半径内的随机点移动,重复 RandomStepCount 次。</item>
/// <item>任意阶段重新发现玩家 → 返回 FailureBD 树重入追击。</item>
/// <item>所有步骤完成仍未发现 → 返回 SuccessBD 树归位/恢复巡逻。</item>
/// </list>
/// </summary>
[TaskName("Investigate Last Known Pos")]
[TaskCategory("BaseGames/Enemy/Movement")]
[TaskDescription("多步搜查:导航到最后已知位置 → 环顾四周 → 随机游走 N 次")]
public sealed class BD_InvestigateLastKnown : Action
{
[Tooltip("到达搜查点后环顾的时长s")]
[SerializeField] private float m_LookAroundDuration = 0.8f;
[Tooltip("随机行走步数(完成环顾后随机游荡的次数)")]
[SerializeField] [Min(0)] private int m_RandomStepCount = 2;
[Tooltip("随机行走半径m")]
[SerializeField] private float m_SearchRadius = 2.5f;
[Tooltip("到达目标点的判定半径m")]
[SerializeField] private float m_ArriveRadius = 0.6f;
private enum InvestigateSubStep { Navigate, LookAround, WalkRandom }
private EnemyBase _enemy;
private InvestigateSubStep _step;
private float _stepTimer;
private int _stepsRemaining;
private Vector2 _randomTarget;
private bool _pathFailed;
private bool _subscribed;
public override void OnAwake() => _enemy = gameObject.GetComponent<EnemyBase>();
public override void OnStart()
{
_enemy.SetAiPhase(AiPhase.Investigate);
_step = InvestigateSubStep.Navigate;
_stepTimer = 0f;
_stepsRemaining = m_RandomStepCount;
_pathFailed = false;
var ac = _enemy.AnimConfig;
if (_enemy.Animancer != null)
{
var clip = ac?.Investigate ?? ac?.Walk;
if (clip != null) _enemy.Animancer.Play(clip);
}
if (!_subscribed && _enemy.Nav != null)
{
_enemy.Nav.OnNavPathFailed += HandlePathFailed;
_subscribed = true;
}
_enemy.MoveTo(_enemy.LastKnownPlayerPosition);
}
public override TaskStatus OnUpdate()
{
if (_enemy == null) return TaskStatus.Failure;
if (_enemy.IsPlayerVisible()) return TaskStatus.Failure;
switch (_step)
{
case InvestigateSubStep.Navigate: return UpdateNavigate();
case InvestigateSubStep.LookAround: return UpdateLookAround();
case InvestigateSubStep.WalkRandom: return UpdateWalkRandom();
default: return TaskStatus.Success;
}
}
public override void OnEnd()
{
if (_subscribed && _enemy?.Nav != null)
{
_enemy.Nav.OnNavPathFailed -= HandlePathFailed;
_subscribed = false;
}
_enemy?.StopMovement();
}
// ── 阶段逻辑 ────────────────────────────────────────────────────
private TaskStatus UpdateNavigate()
{
Vector2 self = _enemy.transform.position;
bool arrived = _pathFailed ||
(self - _enemy.LastKnownPlayerPosition).sqrMagnitude <= m_ArriveRadius * m_ArriveRadius;
if (!arrived) return TaskStatus.Running;
_enemy.StopMovement();
EnterLookAround();
return TaskStatus.Running;
}
private TaskStatus UpdateLookAround()
{
_stepTimer += Time.deltaTime;
if (_stepTimer < m_LookAroundDuration) return TaskStatus.Running;
if (_stepsRemaining > 0)
EnterRandomWalk();
else
return TaskStatus.Success;
return TaskStatus.Running;
}
private TaskStatus UpdateWalkRandom()
{
Vector2 self = _enemy.transform.position;
bool arrived = _pathFailed ||
(self - _randomTarget).sqrMagnitude <= m_ArriveRadius * m_ArriveRadius;
if (!arrived) return TaskStatus.Running;
_enemy.StopMovement();
_stepsRemaining--;
_pathFailed = false;
if (_stepsRemaining > 0)
EnterRandomWalk();
else
return TaskStatus.Success;
return TaskStatus.Running;
}
// ── 辅助方法 ────────────────────────────────────────────────────
private void EnterLookAround()
{
_step = InvestigateSubStep.LookAround;
_stepTimer = 0f;
var ac = _enemy.AnimConfig;
if (_enemy.Animancer != null)
{
var clip = ac?.Investigate ?? ac?.Idle;
if (clip != null) _enemy.Animancer.Play(clip);
}
}
private void EnterRandomWalk()
{
_step = InvestigateSubStep.WalkRandom;
_pathFailed = false;
Vector2 origin = _enemy.LastKnownPlayerPosition;
// 横板地形中只在水平方向随机偏移,保留 origin.y
// PathBerserker2d 寻路负责处理跨平台的纵向路径规划。
float dir = Random.value > 0.5f ? 1f : -1f;
float dist = Random.Range(0.5f, m_SearchRadius);
_randomTarget = new Vector2(origin.x + dir * dist, origin.y);
_enemy.MoveTo(_randomTarget);
var ac = _enemy.AnimConfig;
if (_enemy.Animancer != null)
{
var clip = ac?.Walk;
if (clip != null) _enemy.Animancer.Play(clip);
}
}
private void HandlePathFailed() => _pathFailed = true;
}
}
#endif