- 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.
194 lines
8.5 KiB
C#
194 lines
8.5 KiB
C#
using System;
|
||
using System.Collections;
|
||
using UnityEngine;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Pool;
|
||
|
||
namespace BaseGames.Enemies.Abilities
|
||
{
|
||
/// <summary>
|
||
/// 闪烁突袭能力:消失后瞬移到目标侧后方/上方/侧翼,现身后衔接出手。
|
||
///
|
||
/// 流程:
|
||
/// 1. 选择闪烁目标点(玩家侧后 / 上方 / 侧翼,依 <see cref="BlinkPosition"/>)
|
||
/// 2. 隐身(隐藏 SpriteRenderer、关闭碰撞、播放消失 VFX)
|
||
/// 3. 等待 disappearDuration 营造"消失"感
|
||
/// 4. 瞬移到目标点
|
||
/// 5. 现身 + 出现 VFX + 预警闪光(reappearTelegraph)
|
||
/// 6. 调用 followUpAbilityId 指定的能力作为出手(若为空则播放 attackSequence[0])
|
||
///
|
||
/// 同时实现 <see cref="INavLinkHandler"/> for <see cref="NavLinkType.Teleport"/>:
|
||
/// 当 PathBerserker2d 路径包含 teleport NavLink 时,本能力接管穿越逻辑,
|
||
/// 播放消失/出现动画后告知 NavAgent 继续路径,而不触发战斗出手。
|
||
/// </summary>
|
||
public sealed class BlinkStrikeAbility : EnemyAbilityBase, INavLinkHandler
|
||
{
|
||
public enum BlinkPosition { BehindTarget, FrontTarget, AboveTarget, FlankTarget }
|
||
|
||
[Header("闪烁参数")]
|
||
[SerializeField] private BlinkPosition _blinkPosition = BlinkPosition.BehindTarget;
|
||
[SerializeField] private float _offsetDistance = 1.8f;
|
||
[SerializeField] private float _verticalOffsetAbove = 3.0f;
|
||
[SerializeField] private float _disappearDuration = 0.25f;
|
||
[SerializeField] private float _reappearTelegraph = 0.20f;
|
||
[SerializeField] private LayerMask _groundMask;
|
||
[SerializeField] private float _groundSnapMaxDistance = 3f;
|
||
|
||
[Header("视觉")]
|
||
[SerializeField] private SpriteRenderer[] _renderers;
|
||
[SerializeField] private Collider2D[] _disableDuringBlink;
|
||
[SerializeField] private string _disappearVfxKey = "";
|
||
[SerializeField] private string _appearVfxKey = "";
|
||
|
||
[Header("出手能力(在现身后触发,若为空则播放第一段攻击动画)")]
|
||
[SerializeField] private string _followUpAbilityId = "";
|
||
|
||
private Rigidbody2D _rb;
|
||
private IObjectPoolService _pool;
|
||
private Coroutine _navLinkCoroutine;
|
||
|
||
// ── INavLinkHandler(Teleport 连接段穿越)─────────────────────
|
||
private static readonly NavLinkType[] _handledTypes = new[] { NavLinkType.Teleport };
|
||
public NavLinkType[] HandledLinkTypes => _handledTypes;
|
||
|
||
public bool CanHandleLink(NavLinkType type, Vector2 linkStart, Vector2 linkEnd) => true;
|
||
|
||
public void BeginLinkTraversal(NavLinkType type, Vector2 linkStart, Vector2 linkEnd, Action onComplete)
|
||
{
|
||
if (_navLinkCoroutine != null) StopCoroutine(_navLinkCoroutine);
|
||
_navLinkCoroutine = StartCoroutine(TeleportNavLinkCoroutine(linkEnd, onComplete));
|
||
}
|
||
|
||
public void AbortLinkTraversal()
|
||
{
|
||
if (_navLinkCoroutine != null) { StopCoroutine(_navLinkCoroutine); _navLinkCoroutine = null; }
|
||
SetVisible(true);
|
||
}
|
||
|
||
/// <summary>纯导航用传送:消失 → 移动到连接终点 → 出现,不触发战斗出手。</summary>
|
||
private IEnumerator TeleportNavLinkCoroutine(Vector2 destination, Action onComplete)
|
||
{
|
||
_pool ??= ServiceLocator.GetOrDefault<IObjectPoolService>();
|
||
|
||
if (!string.IsNullOrEmpty(_disappearVfxKey))
|
||
_pool?.Spawn(_disappearVfxKey, _transform.position, Quaternion.identity);
|
||
SetVisible(false);
|
||
if (_rb != null) _rb.velocity = Vector2.zero;
|
||
yield return EnemyAbilityWaits.Get(_disappearDuration);
|
||
|
||
if (_rb != null) _rb.position = destination;
|
||
else _transform.position = destination;
|
||
|
||
SetVisible(true);
|
||
if (!string.IsNullOrEmpty(_appearVfxKey))
|
||
_pool?.Spawn(_appearVfxKey, _transform.position, Quaternion.identity);
|
||
|
||
_navLinkCoroutine = null;
|
||
onComplete?.Invoke(); // 通知 EnemyNavAgent → CompleteLinkTraversal
|
||
}
|
||
|
||
protected override void Awake()
|
||
{
|
||
base.Awake();
|
||
_rb = GetComponentInParent<Rigidbody2D>();
|
||
if (_renderers == null || _renderers.Length == 0)
|
||
_renderers = GetComponentsInChildren<SpriteRenderer>(true);
|
||
}
|
||
|
||
protected override IEnumerator ExecuteCoroutine()
|
||
{
|
||
_pool ??= ServiceLocator.GetOrDefault<IObjectPoolService>();
|
||
var target = _enemy != null ? _enemy.PlayerTransform : null;
|
||
if (target == null) yield break;
|
||
|
||
// 1. 消失
|
||
if (!string.IsNullOrEmpty(_disappearVfxKey))
|
||
_pool?.Spawn(_disappearVfxKey, _transform.position, Quaternion.identity);
|
||
SetVisible(false);
|
||
if (_rb != null) _rb.velocity = Vector2.zero;
|
||
yield return EnemyAbilityWaits.Get(_disappearDuration);
|
||
|
||
// 2. 选目标点 + 瞬移
|
||
Vector2 dest = ComputeBlinkPosition(target);
|
||
if (_rb != null) _rb.position = dest;
|
||
else _transform.position = dest;
|
||
FaceTarget(target);
|
||
|
||
// 3. 现身 + 预警
|
||
SetVisible(true);
|
||
if (!string.IsNullOrEmpty(_appearVfxKey))
|
||
_pool?.Spawn(_appearVfxKey, _transform.position, Quaternion.identity);
|
||
Phase = AbilityRunState.Telegraph;
|
||
yield return EnemyAbilityWaits.Get(_reappearTelegraph);
|
||
|
||
// 4. 出手
|
||
Phase = AbilityRunState.Active;
|
||
if (!string.IsNullOrEmpty(_followUpAbilityId) && _enemy != null)
|
||
{
|
||
var follow = _enemy.Abilities.Get(_followUpAbilityId);
|
||
if (follow != null)
|
||
{
|
||
follow.Execute();
|
||
while (follow.IsRunning) yield return null;
|
||
yield break;
|
||
}
|
||
}
|
||
// 退化路径:播放第一段动画
|
||
var seq = _config != null ? _config.attackSequence : null;
|
||
if (seq != null && seq.Length > 0)
|
||
{
|
||
var atk = seq[0];
|
||
float dur = atk.fallbackDuration;
|
||
if (atk.clip != null && _animancer != null)
|
||
{
|
||
var st = _animancer.Play(atk.clip);
|
||
if (st != null && st.Length > 0f) dur = st.Length;
|
||
}
|
||
yield return EnemyAbilityWaits.Get(dur);
|
||
}
|
||
}
|
||
|
||
protected override void OnInterrupted(InterruptReason reason)
|
||
{
|
||
// 中断时同时终止 NavLink 穿越协程(若正在进行),防止 onComplete 回调污染导航状态
|
||
AbortLinkTraversal();
|
||
}
|
||
|
||
private Vector2 ComputeBlinkPosition(Transform target)
|
||
{
|
||
Vector2 t = target.position;
|
||
Vector2 selfDir = (t - (Vector2)_transform.position);
|
||
float facing = selfDir.x >= 0f ? 1f : -1f;
|
||
Vector2 raw;
|
||
switch (_blinkPosition)
|
||
{
|
||
case BlinkPosition.FrontTarget: raw = t + new Vector2(facing * _offsetDistance, 0f); break;
|
||
case BlinkPosition.AboveTarget: raw = t + new Vector2(0f, _verticalOffsetAbove); break;
|
||
case BlinkPosition.FlankTarget:
|
||
raw = t + new Vector2((UnityEngine.Random.value < 0.5f ? -1f : 1f) * _offsetDistance, 0f);
|
||
break;
|
||
case BlinkPosition.BehindTarget:
|
||
default:
|
||
float tFacing = target.localScale.x >= 0f ? 1f : -1f;
|
||
raw = t - new Vector2(tFacing * _offsetDistance, 0f);
|
||
break;
|
||
}
|
||
// 贴地(避免悬空)
|
||
var hit = Physics2D.Raycast(raw + Vector2.up * 0.5f, Vector2.down,
|
||
_groundSnapMaxDistance + 0.5f, _groundMask);
|
||
if (hit.collider != null) raw.y = hit.point.y + 0.05f;
|
||
return raw;
|
||
}
|
||
|
||
private void SetVisible(bool visible)
|
||
{
|
||
if (_renderers != null)
|
||
for (int i = 0; i < _renderers.Length; i++)
|
||
if (_renderers[i] != null) _renderers[i].enabled = visible;
|
||
if (_disableDuringBlink != null)
|
||
for (int i = 0; i < _disableDuringBlink.Length; i++)
|
||
if (_disableDuringBlink[i] != null) _disableDuringBlink[i].enabled = visible;
|
||
}
|
||
}
|
||
}
|